Merge branch 'feat/admin-self-approve' into 'main'
feat(workflow): admin self-approve override (D3 relaxation для small teams) See merge request 2-6/2-6-4/terravault/ordinis!280
This commit is contained in:
+32
-3
@@ -104,12 +104,41 @@ public class WorkflowRoles {
|
||||
|
||||
/**
|
||||
* True если у actor'а есть указанная workflow role ИЛИ super-role
|
||||
* {@link #ADMIN}. Admin грантит full workflow access без явного
|
||||
* назначения отдельных reviewer/publisher ролей.
|
||||
* (любая admin-эквивалентная — см. {@link #isAdminRole}).
|
||||
* Admin грантит full workflow access без явного назначения отдельных
|
||||
* reviewer/publisher ролей.
|
||||
*/
|
||||
public boolean hasRole(String role) {
|
||||
Set<String> roles = currentRoles();
|
||||
return roles.contains(role) || roles.contains(ADMIN);
|
||||
if (roles.contains(role)) return true;
|
||||
return isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* True если у actor'а есть admin-эквивалентная роль. Match'ит:
|
||||
* <ul>
|
||||
* <li>Literal {@code ordinis:admin} (canonical).</li>
|
||||
* <li>Plain {@code admin} (Keycloak client-role на client {@code ordinis}
|
||||
* без префикса — Альтум realm convention).</li>
|
||||
* <li>Любая роль с tail'ом ADMIN после {@code -}/{@code _}/{@code :}
|
||||
* (e.g. {@code ordinis-admin}, {@code global:admin}, {@code org:admin}).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Зеркало backend {@code ScopeContext.isAdminRole} — что считаем admin
|
||||
* для scope hierarchy expansion, то же считаем admin для workflow override.
|
||||
*/
|
||||
public boolean isAdmin() {
|
||||
return currentRoles().stream().anyMatch(WorkflowRoles::isAdminRole);
|
||||
}
|
||||
|
||||
/** Tail-match: true если role литерально admin или tail после
|
||||
* {@code -}/{@code _}/{@code :} равен ADMIN. */
|
||||
static boolean isAdminRole(String role) {
|
||||
if (role == null || role.isBlank()) return false;
|
||||
String s = role.trim().toUpperCase();
|
||||
int sep = Math.max(Math.max(s.lastIndexOf('-'), s.lastIndexOf('_')), s.lastIndexOf(':'));
|
||||
String tail = sep >= 0 ? s.substring(sep + 1) : s;
|
||||
return "ADMIN".equals(tail);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+8
@@ -265,10 +265,18 @@ public class DraftService {
|
||||
+ " — approve possible только PENDING.");
|
||||
}
|
||||
if (draft.getMakerId() != null && draft.getMakerId().equals(reviewerId)) {
|
||||
// Admin override — D3 maker-checker relaxation для small teams.
|
||||
// Admin'ы (ordinis:admin / admin / *:admin) могут approve свой draft
|
||||
// — solo-founder + small-team scenario. Audit log записывается с
|
||||
// явной пометкой что reviewer == maker, compliance trail сохраняется.
|
||||
if (workflowRoles == null || !workflowRoles.isAdmin()) {
|
||||
throw OrdinisException.conflict(
|
||||
"self_approve_forbidden",
|
||||
"Maker '" + reviewerId + "' не может approve свой draft (D3 maker-checker).");
|
||||
}
|
||||
log.info("Admin self-approve override: draft={} maker={} reviewer={} (same)",
|
||||
draftId, draft.getMakerId(), reviewerId);
|
||||
}
|
||||
DictionaryDefinition def = definitionService.findById(draft.getDictionaryId());
|
||||
// Phase 2: dict-level минимальная роль ревьюера (e.g. "ordinis:restricted").
|
||||
requireDictMinRoleIfSet(def);
|
||||
|
||||
+7
@@ -285,10 +285,17 @@ public class SchemaDraftService {
|
||||
+ " — decision требует review_pending.");
|
||||
}
|
||||
if (draft.getMakerId().equals(reviewerId)) {
|
||||
// Admin override — см. DraftService.approve rationale.
|
||||
// Solo-founder + small-team могут decide свои schema drafts если
|
||||
// у них admin role. Audit log записывает self-decision явно.
|
||||
if (workflowRoles == null || !workflowRoles.isAdmin()) {
|
||||
throw OrdinisException.conflict(
|
||||
"self_approve_forbidden",
|
||||
"Maker '" + reviewerId + "' не может decide свой draft (maker-checker).");
|
||||
}
|
||||
log.info("Admin self-decide override: draft={} maker={} reviewer={} decision={}",
|
||||
draftId, draft.getMakerId(), reviewerId, req.decision());
|
||||
}
|
||||
// Phase 2: dict-level минимальная роль ревьюера. Resolveим def early,
|
||||
// чтобы вернуть 403 forbidden_role до того как меняем status.
|
||||
DictionaryDefinition decideDef = definitionService.findById(draft.getDictionaryId());
|
||||
|
||||
Reference in New Issue
Block a user