Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,56 @@
import java.util.List;

/**
* Portable, environment-independent snapshot of the kit's <em>definitional</em>
* platform config — keyed entirely by natural codes, never DB UUIDs — so it can be
* exported from one environment and imported into another (or committed to git and
* applied on deploy). See ADR 0003.
* Portable, environment-independent snapshot of the kit's platform config — keyed
* entirely by natural codes, never DB UUIDs — so it can be exported from one
* environment and imported into another (or committed to git and applied on deploy).
* See ADR 0003.
*
* <p>Scope: permissions, roles (+ their permission codes), and menus. Operational data
* (users, assignments) and history (audit logs) are deliberately excluded; ABAC policies
* are code, not data.
* <p><strong>Definitional</strong> config (permissions, roles + their permission codes,
* menus) is always present. <strong>Users</strong> are operational data and are only
* included when explicitly requested (export {@code includeUsers=true}); even then no
* secret is ever carried — only login id, email, status and assigned role codes. History
* (audit logs) is never included; ABAC policies are code, not data.
*/
public record ConfigBundle(
int version,
String tenantId,
List<PermissionDef> permissions,
List<RoleDef> roles,
List<MenuDef> menus
List<MenuDef> menus,
List<UserDef> users
) {

/** Bump when the bundle shape changes incompatibly. */
public static final int CURRENT_VERSION = 1;

/** Null-normalises every list so a partially-populated bundle (e.g. hand-pasted JSON) is safe. */
public ConfigBundle {
permissions = permissions == null ? List.of() : permissions;
roles = roles == null ? List.of() : roles;
menus = menus == null ? List.of() : menus;
users = users == null ? List.of() : users;
}

/** Definitional-only bundle (no users) — the common case. */
public ConfigBundle(
int version,
String tenantId,
List<PermissionDef> permissions,
List<RoleDef> roles,
List<MenuDef> menus
) {
this(version, tenantId, permissions, roles, menus, List.of());
}

public record PermissionDef(String code, String description) {
}

/** A role and the permission <em>codes</em> it grants (resolved cross-environment). */
public record RoleDef(String code, String name, List<String> permissionCodes) {
public RoleDef {
permissionCodes = permissionCodes == null ? List.of() : permissionCodes;
}
}

/** A menu item; {@code parentCode} / {@code requiredPermissionCode} reference by code. */
Expand All @@ -41,4 +66,16 @@ public record MenuDef(
int displayOrder
) {
}

/**
* A user account — natural key is {@code loginId} within the tenant. Carries no
* password (transported users are created with no usable password and must have one
* set by an admin) and no environment-specific public id. {@code roleCodes} are the
* roles to assign by code.
*/
public record UserDef(String loginId, String email, String status, List<String> roleCodes) {
public UserDef {
roleCodes = roleCodes == null ? List.of() : roleCodes;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,57 @@
import kr.devslab.kit.access.core.service.PermissionAdminService;
import kr.devslab.kit.access.core.service.RoleAdminService;
import kr.devslab.kit.access.core.service.RolePermissionService;
import kr.devslab.kit.access.core.service.UserRoleService;
import kr.devslab.kit.admin.config.ConfigBundle.MenuDef;
import kr.devslab.kit.admin.config.ConfigBundle.PermissionDef;
import kr.devslab.kit.admin.config.ConfigBundle.RoleDef;
import kr.devslab.kit.admin.config.ConfigBundle.UserDef;
import kr.devslab.kit.core.id.TenantId;
import kr.devslab.kit.identity.UserAccountView;
import kr.devslab.kit.identity.core.service.PlatformUserAccountAdminService;
import kr.devslab.kit.menu.core.entity.PlatformMenuEntity;
import kr.devslab.kit.menu.core.service.MenuAdminService;

/**
* Builds a {@link ConfigBundle} from the live database for a tenant, translating every
* DB id into the corresponding natural code so the result is portable across environments
* (ADR 0003). Read-only.
*
* <p>Definitional config (permissions, roles, menus) is always exported. Users are
* exported only when {@code includeUsers} is set, and even then carry no password —
* just login id, email, status and assigned role codes.
*/
public class ConfigExportService {

private final PermissionAdminService permissions;
private final RoleAdminService roles;
private final RolePermissionService rolePermissions;
private final MenuAdminService menus;
private final PlatformUserAccountAdminService userAccounts;
private final UserRoleService userRoles;

public ConfigExportService(
PermissionAdminService permissions,
RoleAdminService roles,
RolePermissionService rolePermissions,
MenuAdminService menus
MenuAdminService menus,
PlatformUserAccountAdminService userAccounts,
UserRoleService userRoles
) {
this.permissions = permissions;
this.roles = roles;
this.rolePermissions = rolePermissions;
this.menus = menus;
this.userAccounts = userAccounts;
this.userRoles = userRoles;
}

/** Definitional-only export (no users). */
public ConfigBundle export(TenantId tenantId) {
return export(tenantId, false);
}

public ConfigBundle export(TenantId tenantId, boolean includeUsers) {
List<PlatformPermissionEntity> permEntities = permissions.listAll();
Map<UUID, String> permIdToCode = permEntities.stream()
.collect(Collectors.toMap(PlatformPermissionEntity::getId, PlatformPermissionEntity::getCode));
Expand Down Expand Up @@ -74,7 +93,10 @@ public ConfigBundle export(TenantId tenantId) {
menu.getSortOrder()))
.toList();

return new ConfigBundle(ConfigBundle.CURRENT_VERSION, tenantId.value(), permissionDefs, roleDefs, menuDefs);
List<UserDef> userDefs = includeUsers ? exportUsers(tenantId) : List.of();

return new ConfigBundle(
ConfigBundle.CURRENT_VERSION, tenantId.value(), permissionDefs, roleDefs, menuDefs, userDefs);
}

private List<String> permissionCodesFor(Role role, Map<UUID, String> permIdToCode) {
Expand All @@ -84,4 +106,21 @@ private List<String> permissionCodesFor(Role role, Map<UUID, String> permIdToCod
.sorted()
.toList();
}

private List<UserDef> exportUsers(TenantId tenantId) {
Map<UUID, String> roleCodeById = roles.listByTenant(tenantId).stream()
.collect(Collectors.toMap(role -> role.id().value(), Role::code));
return userAccounts.listByTenant(tenantId).stream()
.sorted(Comparator.comparing(UserAccountView::loginId))
.map(user -> new UserDef(
user.loginId(),
user.email(),
user.status().name(),
userRoles.findRoleIdsForUser(user.id()).stream()
.map(roleId -> roleCodeById.get(roleId.value()))
.filter(Objects::nonNull)
.sorted()
.toList()))
.toList();
}
}
Loading
Loading