This commit is contained in:
2025-11-26 23:08:20 +03:00
parent a65fb9c3e4
commit 50b3c6eb1d
42 changed files with 1478 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

14
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

168
pom.xml Normal file
View File

@@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.copperside</groupId>
<artifactId>AuthServer</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>AuthServer</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.5</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- основные версии -->
<springdoc.version>2.8.14</springdoc.version>
<oracle.driver.version>23.4.0.24.05</oracle.driver.version>
<ojdbc.version>23.3.0.23.09</ojdbc.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<swagger-core-jakarta.version>2.2.39</swagger-core-jakarta.version>
<!-- плагины -->
<maven.compiler.plugin.version>3.11.0</maven.compiler.plugin.version>
<maven.enforcer.plugin.version>3.2.1</maven.enforcer.plugin.version>
</properties>
<!-- dependencyManagement -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-core-jakarta</artifactId>
<version>${swagger-core-jakarta.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot Web with Jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Spring Data JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<!-- Oracle JDBC drivers (runtime) -->
<dependency>
<groupId>com.oracle.database.nls</groupId>
<artifactId>orai18n</artifactId>
<version>${ojdbc.version}</version>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<version>${ojdbc.version}</version>
</dependency>
<!-- Swagger / springdoc -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<!-- конфигурирование maven-compiler-plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin.version}</version>
<configuration>
<release>17</release>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!-- оставляем spring-boot-maven-plugin (версия берется от parent) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- Enforcer: базовые правила -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>${maven.enforcer.plugin.version}</version>
<executions>
<execution>
<id>enforce</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<!-- Требуем JDK 17 -->
<requireJavaVersion>
<version>17</version>
</requireJavaVersion>
</rules>
<fail>true</fail>
</configuration>
</execution>
</executions>
</plugin>
<!-- (опционально) dependency plugin для быстрых проверок -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.5.0</version>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,11 @@
package ru.copperside;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AuthServer {
public static void main(String[] args) {
SpringApplication.run(AuthServer.class, args);
}
}

View File

@@ -0,0 +1,27 @@
/*
package ru.copperside.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.copperside.model.authinfo.AuthInfo;
import ru.copperside.service.AuthInfoService;
@RestController
@RequestMapping("/api/authinfo")
@RequiredArgsConstructor
public class AuthInfoController {
private final AuthInfoService service;
// Пример: GET /api/authinfo/user123?type=Secret
@GetMapping("/{dataId}")
public ResponseEntity<AuthInfo> getByDataIdAndType(
@PathVariable String dataId
) {
return ResponseEntity.ok(service.getByDataIdAndType(dataId));
}
}
*/

View File

@@ -0,0 +1,58 @@
package ru.copperside.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.copperside.model.authinfo.SecretData;
import ru.copperside.model.dto.RolePermissionDto;
import ru.copperside.model.enums.AuthenticationType;
import ru.copperside.model.permission.Permission;
import ru.copperside.service.DebugAuthService;
import java.util.List;
@RestController
@RequestMapping("/debug/auth")
@RequiredArgsConstructor
public class DebugAuthController {
private final DebugAuthService service;
/**
* GET /debug/auth/secret/{dataId}?type=Secret
* Если type не указан, используется AuthenticationType.Secret.
*/
@GetMapping("/secret/{dataId}")
public ResponseEntity<SecretData> getSecret(
@PathVariable String dataId,
@RequestParam(required = false) String type
) {
String resolvedType = (type == null || type.isBlank())
? AuthenticationType.Secret.name()
: type;
SecretData secretData = service.getSecretData(dataId, resolvedType);
return ResponseEntity.ok(secretData);
}
@GetMapping("/permission/{hierarchyId}")
public ResponseEntity<List<RolePermissionDto>> getRolePermission(
@PathVariable Long hierarchyId
) {
return ResponseEntity.ok(service.byHierarchy(hierarchyId));
}
@GetMapping("/personal_permission/{hierarchyId}")
public ResponseEntity<List<RolePermissionDto>> getPersonalPermission(
@PathVariable Long hierarchyId
) {
return ResponseEntity.ok(service.PersonalbyHierarchy(hierarchyId));
}
@GetMapping("/permissions/{hierarchyId}")
public ResponseEntity<List<Permission>> getPermissions(
@PathVariable Long hierarchyId
) {
return ResponseEntity.ok(service.getPermissions(hierarchyId));
}
}

View File

@@ -0,0 +1,25 @@
package ru.copperside.model.authinfo;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Builder;
import lombok.extern.jackson.Jacksonized;
import ru.copperside.model.permission.Permission;
import ru.copperside.model.session.SessionSettings;
import java.util.List;
@Jacksonized
@Builder
public record AuthInfo(
Long authId,
String dataId,
Long hierarchyId,
String displayName,
Boolean isEnabled,
Boolean needActivation,
List<Permission> permissions,
SessionSettings sessionSettings,
JsonNode sessionData,
JsonNode privateData,
SecretData secretData
) { }

View File

@@ -0,0 +1,19 @@
package ru.copperside.model.authinfo;
import lombok.Builder;
import lombok.extern.jackson.Jacksonized;
import ru.copperside.model.enums.AuthenticationType;
import ru.copperside.model.enums.KeyUsage;
import ru.copperside.model.enums.SecretType;
import java.util.EnumSet;
@Jacksonized
@Builder
public record SecretData(
AuthenticationType authType,
Boolean isEnabled,
EnumSet<KeyUsage> keyUsages,
SecretType secretType,
String secret
) {}

View File

@@ -0,0 +1,8 @@
package ru.copperside.model.dto;
import com.fasterxml.jackson.annotation.JsonAlias;
public record ParametersDTO(
@JsonAlias({"IsEnabled","isEnabled"}) Boolean isEnabled,
@JsonAlias({"NeedActivation","needActivation"}) Boolean needActivation
) {}

View File

@@ -0,0 +1,14 @@
package ru.copperside.model.dto;
public record RolePermissionDto(
Long hierarchyId,
Integer level,
Long roleId,
Long permissionId,
String permissionStrId,
String settingsJson,
Boolean action,
String pDataJson,
String command,
String http
) { }

View File

@@ -0,0 +1,10 @@
package ru.copperside.model.dto;
import com.fasterxml.jackson.annotation.JsonAlias;
public record SecretDTO(
@JsonAlias({"IsEnable","isEnable"}) Boolean isEnable,
@JsonAlias({"KeyUsage","keyUsage"}) Object keyUsage,
@JsonAlias({"SecretType","secretType"}) String secretType,
@JsonAlias({"Secret","secret"}) String secret
) {}

View File

@@ -0,0 +1,21 @@
package ru.copperside.model.enums;
public enum AuthType {
Unknown(0),
Simple(1),
TwoStep(2);
private final int code;
AuthType(int code) { this.code = code; }
public int code() { return code; }
public static AuthType fromCode(int code) {
return switch (code) {
case 0 -> Unknown;
case 1 -> Simple;
case 2 -> TwoStep;
default -> throw new IllegalArgumentException("Unknown AuthType code: " + code);
};
}
}

View File

@@ -0,0 +1,16 @@
package ru.copperside.model.enums;
public enum AuthenticationType {
Secret,
Windows;
/** Возвращает enum по имени, default = Secret. */
public static AuthenticationType from(String name) {
if (name == null) return Secret;
try {
return AuthenticationType.valueOf(name);
} catch (IllegalArgumentException ex) {
return Secret;
}
}
}

View File

@@ -0,0 +1,21 @@
package ru.copperside.model.enums;
public enum KeyUsage {
None(0),
Password(1),
HMac(2);
private final int code;
KeyUsage(int code) { this.code = code; }
public int code() { return code; }
public static KeyUsage fromCode(int code) {
return switch (code) {
case 0 -> None;
case 1 -> Password;
case 2 -> HMac;
default -> throw new IllegalArgumentException("Unknown KeyUsage code: " + code);
};
}
}

View File

@@ -0,0 +1,6 @@
package ru.copperside.model.enums;
public enum SecretType {
EncodedData,
PlainTextData
}

View File

@@ -0,0 +1,18 @@
package ru.copperside.model.permission;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Builder;
import lombok.extern.jackson.Jacksonized;
@Jacksonized
@Builder(toBuilder = true)
public record Permission(
Long permissionId,
String permissionStrId,
PermissionSettings settings,
Boolean action,
JsonNode permissionData,
String command,
String http
) {}

View File

@@ -0,0 +1,21 @@
package ru.copperside.model.permission;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Builder;
import lombok.extern.jackson.Jacksonized;
@Jacksonized
@Builder(toBuilder = true)
public record PermissionSettings(
Boolean needLog,
Boolean requestLog,
Boolean needConfirmationCode
) {
public PermissionSettings normalize() {
return this.toBuilder()
.needLog(Boolean.TRUE.equals(needLog))
.requestLog(Boolean.TRUE.equals(requestLog))
.needConfirmationCode(Boolean.TRUE.equals(needConfirmationCode))
.build();
}
}

View File

@@ -0,0 +1,20 @@
package ru.copperside.model.session;
import lombok.Builder;
import lombok.extern.jackson.Jacksonized;
import ru.copperside.model.enums.AuthType;
import java.time.Duration;
import java.util.List;
@Jacksonized
@Builder
public record SessionSettings(
Duration ttl,
Boolean autoProlongation,
AuthType authType,
List<String> authStepTypes,
Boolean ignoreConfirmation,
Boolean oneActiveSession,
Boolean inMemory
) {}

View File

@@ -0,0 +1,10 @@
package ru.copperside.repository;
import ru.copperside.model.authinfo.AuthInfo;
import java.util.Optional;
public interface AuthInfoRepository {
Optional<AuthInfo> findByDataIdAndType(String dataId, String type);
Optional<AuthInfo> findByAuthId(Long authId);
}

View File

@@ -0,0 +1,250 @@
/*
package ru.copperside.repository;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import ru.copperside.model.authinfo.*;
import ru.copperside.model.enums.AuthType;
import ru.copperside.model.enums.KeyUsage;
import ru.copperside.model.enums.SecretType;
import ru.copperside.model.session.SessionSettings;
import ru.copperside.sql.SqlRegistry;
import ru.copperside.util.SessionSettingsHelper;
import static ru.copperside.util.RepoMappingHelper.*;
import java.sql.ResultSet;
import java.time.Duration;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
@Repository
@RequiredArgsConstructor
public class AuthInfoRepositoryJdbc implements AuthInfoRepository {
private final NamedParameterJdbcTemplate jdbc;
private final ObjectMapper om;
private final SqlRegistry sqlRegistry;
private static final String SQL_INIT = "auth/find_authinfo_init_by_dataid_and_type";
private static final String SQL_PRIV = "auth/find_privatedata_by_authid";
private static final String SQL_ROLESET = "auth/find_rolesettings_by_authid";
@Override
public Optional<AuthInfo> findByDataIdAndType(String dataId, String type) {
var params = new MapSqlParameterSource()
.addValue("dataId", dataId)
.addValue("type", type);
RowMapper<AuthInfo> rm = (rs, rn) -> mapAuthInfo(rs);
List<AuthInfo> list = jdbc.query(sqlRegistry.get(SQL_INIT), params, rm);
if (!list.isEmpty()) {
AuthInfo base = list.get(0);
if (base.authId() != null) {
JsonNode privateData = loadPrivateData(base.authId());
return Optional.of(base.toBuilder().privateData(privateData).build());
}
}
return Optional.empty();
}
private JsonNode loadPrivateData(Long authId) {
if (authId == null) return null;
var params = new MapSqlParameterSource().addValue("authId", authId);
ObjectNode pd = om.createObjectNode();
jdbc.query(sqlRegistry.get(SQL_PRIV), params, rs -> {
String key = rs.getString("KEY");
String value = rs.getString("VALUE");
try {
JsonNode node = om.readTree(value);
pd.set(key, node);
} catch (Exception e) {
pd.put(key, value);
}
});
return pd;
}
private AuthInfo mapAuthInfo(ResultSet rs) {
try {
Long authId = getLong(rs, "AD_AUTHID"); // AD_AUTHID -> AuthInfo.authId
String dataId = rs.getString("AD_DATAID"); // AD_DATAID -> AuthInfo.dataId
Long hierarchyId = getLong(rs, "AH_HIERARCHYID"); // AH_HIERARCHYID -> AuthInfo.hierarchyId
String displayName = rs.getString("AH_DISPLAYNAME"); // AH_DISPLAYNAME -> AuthInfo.displayName
String secretType = rs.getString("AD_TYPE");
Boolean isEnabled = null;
Boolean needActivation = null;
JsonNode paramsJson = readJsonFlexible(rs, "AI_PARAMETERS", om);
if (paramsJson != null && !paramsJson.isNull()) {
isEnabled = getBooleanCaseInsensitive(paramsJson, "IsEnabled", "isEnabled");
needActivation = getBooleanCaseInsensitive(paramsJson, "NeedActivation", "needActivation");
}
SecretData secretData = null;
JsonNode secretJson = readJsonFlexible(rs, "AD_DATA", om);
if (secretJson != null && !secretJson.isNull()) {
Boolean sdIsEnabled = getBooleanCaseInsensitive(secretJson, "IsEnable", "isEnabled");
String keyUsageStr = getStringCaseInsensitive(secretJson, "KeyUsage", "keyUsage");
String secretTypeStr = getStringCaseInsensitive(secretJson, "SecretType", "secretType");
String secretValue = getStringCaseInsensitive(secretJson, "Secret", "secret");
EnumSet<KeyUsage> keyUsages = parseKeyUsageSet(secretJson, keyUsageStr);
SecretType sType = parseSecretType(secretTypeStr);
secretData = SecretData.builder()
.type(secretType) // тип секрета из AD_TYPE (внешний)
.isEnabled(sdIsEnabled)
.keyUsage(keyUsages)
.secretType(sType)
.secret(secretValue)
.build();
}else {
secretData = SecretData.builder()
.type(secretType)
.build();
}
JsonNode aiSettingsJson = readJsonFlexible(rs, "AI_SETTINGS", om);
SessionSettings sessionSettings = computeSessionSettings(authId, aiSettingsJson); // ⬅️ главный расчёт
return AuthInfo.builder()
.authId(authId)
.dataId(dataId)
.hierarchyId(hierarchyId)
.displayName(displayName)
.isEnabled(isEnabled)
.needActivation(needActivation)
.permissions(List.of())
.sessionSettings(sessionSettings)
.sessionData(null)
.privateData(null)
.secretData(secretData)
.build();
} catch (Exception e) {
throw new IllegalStateException("Failed to map AuthInfo", e);
}
}
private SessionSettings computeSessionSettings(Long authId, JsonNode aiSettingsJson) {
// userSettings = {}; merge(AI_SETTINGS, false)
SessionSettings userSettings = SessionSettingsHelper.merge(
SessionSettings.builder().build(),
mapSessionSettings(aiSettingsJson),
false
);
// Ролевые настройки с ORDER BY LEVEL (накатываем последовательно isRole=true)
var resultRef = new AtomicReference<>(SessionSettings.builder().build());
var params = new MapSqlParameterSource().addValue("authId", authId);
jdbc.query(sqlRegistry.get(SQL_ROLESET), params, rs -> {
JsonNode roleJson = readJsonFlexible(rs, "SETTINGS", om);
SessionSettings roleSet = mapSessionSettings(roleJson);
resultRef.set(SessionSettingsHelper.merge(resultRef.get(), roleSet, true));
});
// финально поверх — userSettings (isRole=false)
return SessionSettingsHelper.merge(resultRef.get(), userSettings, false);
}
private SessionSettings mapSessionSettings(JsonNode node) {
if (node == null || node.isNull()) return null;
Duration ttl = null;
if (node.hasNonNull("ttlSec")) {
ttl = Duration.ofSeconds(node.path("ttlSec").asLong(0));
} else if (node.hasNonNull("ttl")) {
try { ttl = Duration.parse(node.path("ttl").asText()); } catch (Exception ignore) {}
}
AuthType authType = null;
if (node.hasNonNull("authType")) {
var at = node.get("authType");
if (at.isInt()) {
authType = AuthType.fromCode(at.asInt(0));
} else {
try { authType = AuthType.valueOf(at.asText()); } catch (Exception ignore) {}
}
}
List<String> steps = new ArrayList<>();
if (node.hasNonNull("authStepTypes") && node.get("authStepTypes").isArray()) {
for (JsonNode n : node.get("authStepTypes")) {
steps.add(n.asText());
}
}
return new SessionSettingsBuilder()
.ttl(ttl)
.autoProlongation(asNullableBool(node, "autoProlongation"))
.authType(authType)
.authStepTypes(steps)
.ignoreConfirmation(asNullableBool(node, "ignoreConfirmation"))
.oneActiveSession(asNullableBool(node, "oneActiveSession"))
.inMemory(asNullableBool(node, "inMemory"))
.build();
}
private Boolean asNullableBool(JsonNode node, String field) {
return node.has(field) && !node.get(field).isNull() ? node.get(field).asBoolean() : null;
}
private static final class SessionSettingsBuilder {
private Duration ttl;
private Boolean autoProlongation;
private AuthType authType;
private List<String> authStepTypes;
private Boolean ignoreConfirmation;
private Boolean oneActiveSession;
private Boolean inMemory;
SessionSettingsBuilder() {}
SessionSettingsBuilder(SessionSettings base) {
if (base == null) return;
this.ttl = base.ttl();
this.autoProlongation = base.autoProlongation();
this.authType = base.authType();
this.authStepTypes = base.authStepTypes();
this.ignoreConfirmation = base.ignoreConfirmation();
this.oneActiveSession = base.oneActiveSession();
this.inMemory = base.inMemory();
}
SessionSettingsBuilder ttl(Duration v) { this.ttl = v; return this; }
SessionSettingsBuilder autoProlongation(Boolean v) { this.autoProlongation = v; return this; }
SessionSettingsBuilder authType(AuthType v) { this.authType = v; return this; }
SessionSettingsBuilder authStepTypes(List<String> v) { this.authStepTypes = v; return this; }
SessionSettingsBuilder ignoreConfirmation(Boolean v) { this.ignoreConfirmation = v; return this; }
SessionSettingsBuilder oneActiveSession(Boolean v) { this.oneActiveSession = v; return this; }
SessionSettingsBuilder inMemory(Boolean v) { this.inMemory = v; return this; }
SessionSettings build() {
return SessionSettings.builder()
.ttl(ttl)
.autoProlongation(autoProlongation)
.authType(authType)
.authStepTypes(authStepTypes == null ? null : List.copyOf(authStepTypes))
.ignoreConfirmation(ignoreConfirmation)
.oneActiveSession(oneActiveSession)
.inMemory(inMemory)
.build();
}
}
}
*/

View File

@@ -0,0 +1,12 @@
package ru.copperside.repository;
import ru.copperside.model.dto.RolePermissionDto;
import ru.copperside.model.permission.Permission;
import java.util.List;
public interface PermissionsRepository {
List<RolePermissionDto> findRoleByHierarchyId(Long hierarchyId);
List<RolePermissionDto> findPersonalByHierarchyId(Long hierarchyId);
List<Permission> findCompiledByHierarchyId(Long hierarchyId);
}

View File

@@ -0,0 +1,70 @@
package ru.copperside.repository;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import ru.copperside.model.dto.RolePermissionDto;
import ru.copperside.sql.SqlRegistry;
import ru.copperside.util.PermissionsCompiler;
import ru.copperside.model.permission.Permission;
import java.util.List;
import static ru.copperside.util.RepoMappingHelper.*;
@Repository
@RequiredArgsConstructor
public class PermissionsRepositoryJdbc implements PermissionsRepository{
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
private final SqlRegistry sqlRegistry;
private final ObjectMapper objectMapper;
@Override
public List<RolePermissionDto> findRoleByHierarchyId(Long hierarchyId) {
var p = new MapSqlParameterSource().addValue("hierarchyId", hierarchyId);
return namedParameterJdbcTemplate.query(sqlRegistry.get("auth/find_inherited_role_permissions_by_hierarchyid"), p, (rs, rn) -> {
return new RolePermissionDto(
rs.getLong("HIERARCHYID"),
rs.getInt("LEVEL"),
rs.getLong("ROLEID"),
rs.getLong("PERMISSIONID"),
rs.getString("PERMISSION_STRID"),
readLargeText(rs, "SETTINGS"),
getNullableBoolean(rs, "ACTION"),
readLargeText(rs, "PDATA"),
rs.getString("COMMAND"),
rs.getString("HTTP")
);
});
}
@Override
public List<RolePermissionDto> findPersonalByHierarchyId(Long hierarchyId) {
var p = new MapSqlParameterSource().addValue("hierarchyId", hierarchyId);
return namedParameterJdbcTemplate.query(sqlRegistry.get("auth/find_inherited_permissions_by_hierarchyid"), p, (rs, rn) ->
new RolePermissionDto(
getNullableLong(rs, "HIERARCHYID"),
rs.getInt("LEVEL"),
getNullableLong(rs, "ROLEID"), // здесь всегда null из SQL
rs.getLong("PERMISSIONID"),
rs.getString("PERMISSION_STRID"),
readLargeText(rs, "SETTINGS"),
getNullableBoolean(rs, "ACTION"),
readLargeText(rs, "PDATA"),
rs.getString("COMMAND"),
rs.getString("HTTP")
)
);
}
@Override
public List<Permission> findCompiledByHierarchyId(Long hierarchyId) {
var rolePerms = findRoleByHierarchyId(hierarchyId);
var personalPerms = findPersonalByHierarchyId(hierarchyId);
return new PermissionsCompiler(objectMapper).compile(rolePerms, personalPerms);
}
}

View File

@@ -0,0 +1,7 @@
package ru.copperside.repository;
import com.fasterxml.jackson.databind.JsonNode;
public interface PrivateDataRepository {
JsonNode findByAuthId(Long authId);
}

View File

@@ -0,0 +1,10 @@
package ru.copperside.repository;
import ru.copperside.model.authinfo.*;
import java.util.Optional;
public interface SecretRepository {
Optional<SecretData> findByDataIdAndType(String dataId, String type);
}

View File

@@ -0,0 +1,70 @@
package ru.copperside.repository;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import ru.copperside.model.enums.AuthenticationType;
import ru.copperside.model.authinfo.SecretData;
import ru.copperside.model.enums.SecretType;
import ru.copperside.model.enums.KeyUsage;
import ru.copperside.model.dto.SecretDTO;
import ru.copperside.repository.SecretRepository;
import ru.copperside.sql.SqlRegistry;
import java.util.EnumSet;
import java.util.Optional;
import static ru.copperside.util.RepoMappingHelper.*;
@Repository
@RequiredArgsConstructor
public class SecretRepositoryJdbc implements SecretRepository {
private final NamedParameterJdbcTemplate jdbc;
private final ObjectMapper om;
private final SqlRegistry sql;
private final ObjectReader secretReader(ObjectMapper om) {
return om.readerFor(SecretDTO.class);
}
@Override
public Optional<SecretData> findByDataIdAndType(String dataId, String type) {
var p = new MapSqlParameterSource()
.addValue("dataId", dataId)
.addValue("type", (type == null || type.isBlank())
? AuthenticationType.Secret.name()
: type);
var list = jdbc.query(sql.get("auth/find_secret_by_dataid_and_type"), p, (rs, rn) -> {
// тип аутентификации из AD_TYPE, по умолчанию Secret
var authType = AuthenticationType.from(rs.getString("AD_TYPE"));
// читаем AD_DATA -> SecretDTO
SecretDTO dto = readJson(rs, "AD_DATA", secretReader(om));
if (dto == null) {
return SecretData.builder()
.authType(authType)
.keyUsages(EnumSet.of(KeyUsage.None))
.build();
}
EnumSet<KeyUsage> usages = parseKeyUsages(dto.keyUsage());
SecretType sType = parseSecretType(dto.secretType());
return SecretData.builder()
.authType(authType)
.isEnabled(dto.isEnable())
.keyUsages(usages.isEmpty() ? EnumSet.of(KeyUsage.None) : usages)
.secretType(sType)
.secret(dto.secret())
.build();
});
return list.stream().findFirst();
}
}

View File

@@ -0,0 +1,7 @@
package ru.copperside.repository;
import ru.copperside.model.session.SessionSettings;
public interface SettingsRepository {
SessionSettings loadMerged(Long authId);
}

View File

@@ -0,0 +1,24 @@
/*package ru.copperside.service;
import lombok.RequiredArgsConstructor;
import org.springframework.data.crossstore.ChangeSetPersister;
import org.springframework.stereotype.Service;
import ru.copperside.model.authinfo.AuthInfo;
import ru.copperside.repository.AuthInfoRepository;
@Service
@RequiredArgsConstructor
public class AuthInfoService {
private final AuthInfoRepository repo;
public AuthInfo getByDataIdAndType(String dataId) {
return repo.findByDataIdAndType(dataId, "Secret")
.orElseThrow(() -> new NotFoundException(
"AuthInfo(init) not found by dataId=" + dataId + ", type=Secret"));
}
public static class NotFoundException extends RuntimeException {
public NotFoundException(String msg) { super(msg); }
}
}
*/

View File

@@ -0,0 +1,45 @@
package ru.copperside.service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.copperside.model.authinfo.SecretData;
import ru.copperside.model.dto.RolePermissionDto;
import ru.copperside.model.enums.AuthenticationType;
import ru.copperside.model.permission.Permission;
import ru.copperside.repository.PermissionsRepository;
import ru.copperside.repository.SecretRepository;
import java.util.List;
@Service
@RequiredArgsConstructor
public class DebugAuthService {
private final SecretRepository secretRepository;
private final PermissionsRepository permissionsRepository;
public SecretData getSecretData(String dataId, String type) {
// если не указали type — считаем Secret
String resolvedType = (type == null || type.isBlank())
? AuthenticationType.Secret.name()
: type;
return secretRepository.findByDataIdAndType(dataId, resolvedType)
.orElseThrow(() ->
new IllegalArgumentException("Secret not found: dataId=" + dataId + ", type=" + resolvedType));
}
public List<RolePermissionDto> byHierarchy(Long hierarchyId) {
return permissionsRepository.findRoleByHierarchyId(hierarchyId);
}
public List<RolePermissionDto> PersonalbyHierarchy(Long hierarchyId) {
return permissionsRepository.findPersonalByHierarchyId(hierarchyId);
}
public List<Permission> getPermissions(Long hierarchyId) {
return permissionsRepository.findCompiledByHierarchyId(hierarchyId);
}
}

View File

@@ -0,0 +1,65 @@
package ru.copperside.sql;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class SqlRegistry {
private final Map<String, String> sqls = new ConcurrentHashMap<>();
private final ResourcePatternResolver resolver;
public SqlRegistry(ResourcePatternResolver resolver) {
this.resolver = resolver;
}
@PostConstruct
void init() {
try {
// важно: слэш после classpath*:
Resource[] resources = resolver.getResources("classpath*:/sql/**/*.sql");
int count = 0;
for (Resource res : resources) {
String key = buildKey(res);
String sql = new String(res.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim();
sqls.put(key, sql);
count++;
}
log.info("SqlRegistry loaded {} sql files", count);
} catch (IOException e) {
throw new IllegalStateException("Failed to load SQL resources", e);
}
}
private String buildKey(Resource res) throws IOException {
// поддерживает и jar:...!/BOOT-INF/classes!/sql/...
String url = res.getURL().toString();
int idx = url.indexOf("/sql/");
if (idx >= 0) {
String afterSql = url.substring(idx + 5); // после "/sql/"
// убираем всё до возможного "!" для jar-URL (на всякий)
int bang = afterSql.indexOf('!');
if (bang >= 0) afterSql = afterSql.substring(bang + 1);
return afterSql.replaceFirst("\\.sql$", "");
}
// fallback — по имени файла
String name = res.getFilename();
if (name == null) throw new IOException("No filename for resource: " + url);
return name.replaceFirst("\\.sql$", "");
}
public String get(String key) {
String sql = sqls.get(key);
if (sql == null) throw new IllegalArgumentException("SQL not found: " + key);
return sql;
}
}

View File

@@ -0,0 +1,123 @@
package ru.copperside.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import ru.copperside.model.dto.RolePermissionDto;
import ru.copperside.model.permission.Permission;
import ru.copperside.model.permission.PermissionSettings;
import java.util.*;
import java.util.stream.Collectors;
@RequiredArgsConstructor
public class PermissionsCompiler {
private final ObjectMapper objectMapper;
public List<Permission> compile(List<RolePermissionDto> rolePermissions,
List<RolePermissionDto> personalPermissions) {
Map<Long, Permission> compiled = new LinkedHashMap<>();
int maxLevel = Math.max(
rolePermissions.stream()
.map(rp -> Optional.ofNullable(rp.level()).orElse(0))
.max(Integer::compareTo)
.orElse(0),
personalPermissions.stream()
.map(p -> Optional.ofNullable(p.level()).orElse(0))
.max(Integer::compareTo)
.orElse(0)
);
for (int level = 1; level <= maxLevel; level++) {
final int lvl = level;
rolePermissions.stream()
.filter(rp -> Objects.equals(rp.level(), lvl))
.forEach(rp -> {
PermissionSettings settings = deserializeSettings(rp.settingsJson());
JsonNode pdata = deserializeJson(rp.pDataJson());
if (compiled.containsKey(rp.permissionId())) {
Permission existing = compiled.get(rp.permissionId());
existing = existing.toBuilder()
.action(true)
.permissionData(pdata)
.build();
compiled.put(rp.permissionId(), existing);
} else {
compiled.put(rp.permissionId(),
Permission.builder()
.permissionId(rp.permissionId())
.permissionStrId(rp.permissionStrId())
.settings(settings)
.action(true)
.permissionData(pdata)
.command(rp.command())
.http(rp.http())
.build());
}
});
// ==== персональные (иерархические) ====
personalPermissions.stream()
.filter(p -> Objects.equals(p.level(), lvl))
.forEach(p -> {
PermissionSettings settings = deserializeSettings(p.settingsJson());
JsonNode pdata = deserializeJson(p.pDataJson());
if (compiled.containsKey(p.permissionId())) {
Permission existing = compiled.get(p.permissionId());
existing = existing.toBuilder()
.action(Boolean.TRUE.equals(p.action()))
.permissionData(pdata)
.build();
compiled.put(p.permissionId(), existing);
} else {
compiled.put(p.permissionId(),
Permission.builder()
.permissionId(p.permissionId())
.permissionStrId(p.permissionStrId())
.settings(settings)
.action(Boolean.TRUE.equals(p.action()))
.permissionData(pdata)
.command(p.command())
.http(p.http())
.build());
}
});
}
// только активные
return compiled.values().stream()
.filter(Permission::action)
.collect(Collectors.toList());
}
/* ===== helpers ===== */
private PermissionSettings deserializeSettings(String json) {
if (json == null || json.isBlank()) return null;
try {
var s = objectMapper.readValue(json, PermissionSettings.class);
return s == null ? null : s.normalize();
}
catch (Exception e) { return null; }
}
private JsonNode deserializeJson(String json) {
try {
if (json == null || json.isBlank()) {
return objectMapper.createObjectNode(); // <-- пустой объект вместо null
}
JsonNode n = objectMapper.readTree(json);
// гарантируем объект
return (n == null || !n.isObject()) ? objectMapper.createObjectNode() : n;
} catch (Exception e) {
return objectMapper.createObjectNode(); // на ошибке тоже пустой объект
}
}
}

View File

@@ -0,0 +1,82 @@
package ru.copperside.util;
import java.io.Reader;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.EnumSet;
import java.util.List;
import ru.copperside.model.enums.KeyUsage;
import ru.copperside.model.enums.SecretType;
public final class RepoMappingHelper {
private RepoMappingHelper() {}
public static Long getNullableLong(ResultSet rs, String col) throws SQLException {
long v = rs.getLong(col);
return rs.wasNull() ? null : v;
}
public static Boolean getNullableBoolean(ResultSet rs, String col) {
try {
boolean v = rs.getBoolean(col);
return rs.wasNull() ? null : v;
} catch (Exception e) {
return null;
}
}
public static <T> T readJson(ResultSet rs, String col, com.fasterxml.jackson.databind.ObjectReader reader) {
try (Reader r = rs.getCharacterStream(col)) {
if (r != null) return reader.readValue(r);
} catch (Exception ignore) {}
try {
String s = rs.getString(col);
return (s == null || s.isBlank()) ? null : reader.readValue(s);
} catch (Exception ignore) {}
return null;
}
/** Универсальное чтение CLOB / VARCHAR */
public static String readLargeText(ResultSet rs, String col) {
// 1) CLOB → Reader
try (Reader r = rs.getCharacterStream(col)) {
if (r != null) {
var sw = new java.io.StringWriter();
r.transferTo(sw);
return sw.toString();
}
} catch (Exception ignore) {}
// 2) Fallback: обычная строка
try {
return rs.getString(col);
} catch (Exception ignore) {}
return null;
}
public static EnumSet<KeyUsage> parseKeyUsages(Object raw) {
EnumSet<KeyUsage> set = EnumSet.noneOf(KeyUsage.class);
if (raw == null) return EnumSet.of(KeyUsage.None);
if (raw instanceof String s) {
for (String p : s.split("\\s*,\\s*")) addUsage(set, p);
} else if (raw instanceof List<?> list) {
for (Object o : list) addUsage(set, String.valueOf(o));
}
return set.isEmpty() ? EnumSet.of(KeyUsage.None) : set;
}
private static void addUsage(EnumSet<KeyUsage> set, String v) {
switch (v.trim().toLowerCase()) {
case "password" -> set.add(KeyUsage.Password);
case "hmac", "hmacsha256" -> set.add(KeyUsage.HMac);
case "none", "" -> set.add(KeyUsage.None);
default -> {}
}
}
public static SecretType parseSecretType(String s) {
if (s == null) return null;
try { return SecretType.valueOf(s); } catch (IllegalArgumentException ex) { return null; }
}
}

View File

@@ -0,0 +1,64 @@
package ru.copperside.util;
import ru.copperside.model.enums.AuthType;
import ru.copperside.model.session.SessionSettings;
import java.time.Duration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public final class SessionSettingsHelper {
private SessionSettingsHelper() {}
public static SessionSettings merge(SessionSettings target, SessionSettings merge, boolean isRole) {
if (target == null && merge == null) return null;
if (target == null) target = SessionSettings.builder().build();
if (merge == null) return target;
Duration ttl = target.ttl();
Boolean auto = target.autoProlongation();
AuthType at = target.authType();
List<String> steps = target.authStepTypes();
Boolean ign = target.ignoreConfirmation();
Boolean one = target.oneActiveSession();
Boolean mem = target.inMemory();
if (isRole) {
if (merge.ttl() != null) ttl = merge.ttl();
if (merge.autoProlongation() != null) auto = merge.autoProlongation();
if (merge.authType() != null) at = (at == null) ? merge.authType() : max(at, merge.authType());
if (merge.authStepTypes() != null && !merge.authStepTypes().isEmpty()) {
Set<String> s = new LinkedHashSet<>();
if (steps != null) s.addAll(steps);
s.addAll(merge.authStepTypes());
steps = List.copyOf(s);
}
if (merge.ignoreConfirmation() != null) {
if (ign == null) ign = merge.ignoreConfirmation();
else if (!merge.ignoreConfirmation()) ign = false;
}
} else {
if (merge.ttl() != null) ttl = merge.ttl();
if (merge.autoProlongation() != null) auto = merge.autoProlongation();
if (merge.authType() != null) at = merge.authType();
if (merge.authStepTypes() != null) steps = merge.authStepTypes();
if (merge.ignoreConfirmation() != null) ign = merge.ignoreConfirmation();
if (merge.oneActiveSession() != null) one = merge.oneActiveSession();
}
mem = Boolean.TRUE.equals(mem) || Boolean.TRUE.equals(merge.inMemory());
return SessionSettings.builder()
.ttl(ttl).autoProlongation(auto).authType(at)
.authStepTypes(steps)
.ignoreConfirmation(ign).oneActiveSession(one).inMemory(mem)
.build();
}
private static AuthType max(AuthType a, AuthType b) {
int ca = a == null ? 0 : a.ordinal();
int cb = b == null ? 0 : b.ordinal();
return (ca >= cb) ? a : b; // Unknown < Simple < TwoStep
}
}

View File

@@ -0,0 +1,9 @@
server:
port: 8080
spring:
datasource:
url: jdbc:oracle:thin:@127.0.0.1:1521/TKBPAYPDB
username: WFMPROP
password: WFMPROP
driver-class-name: oracle.jdbc.OracleDriver

View File

@@ -0,0 +1,21 @@
SELECT
-- из AUTHDATA
ad.AUTHID AS AD_AUTHID,
ad.TYPE AS AD_TYPE,
ad.DATAID AS AD_DATAID,
ad.DATA AS AD_DATA,
-- из AUTHHIERARCHY + AUTHIDS
ah.HIERARCHYID AS AH_HIERARCHYID,
ai.AUTHID AS AI_AUTHID,
ai.SETTINGS AS AI_SETTINGS,
ai.PARAMETERS AS AI_PARAMETERS,
ah.DISPLAYNAME AS AH_DISPLAYNAME
FROM AUTHDATA ad
LEFT JOIN AUTHIDS ai
ON ai.AUTHID = ad.AUTHID
LEFT JOIN AUTHHIERARCHY ah
ON ah.AUTHID = ai.AUTHID
WHERE ad.DATAID = :dataId
AND ad.TYPE = :type

View File

@@ -0,0 +1,24 @@
SELECT
hp.HIERARCHYID AS HIERARCHYID,
h."LEVEL" AS "LEVEL",
/* у иерархических прав нет роли: оставим NULL как роль */
NULL AS ROLEID,
hp.PERMISSIONID AS PERMISSIONID,
p.STRID AS PERMISSION_STRID,
p.SETTINGS AS SETTINGS,
hp.ACTION AS ACTION,
hp.PERMISSIONDATA AS PDATA,
p.COMMAND AS COMMAND,
p.HTTP AS HTTP
FROM HIERARCHYPERMISSION hp
JOIN PERMISSIONS p ON p.PERMISSIONID = hp.PERMISSIONID
JOIN (
SELECT ah.HIERARCHYID, ah."LEVEL"
FROM AUTHHIERARCHY ah, AUTHHIERARCHY ah2
WHERE ah.LEFTKEY <= ah2.LEFTKEY
AND ah.RIGHTKEY >= ah2.RIGHTKEY
AND ah2.HIERARCHYID = :hierarchyId
ORDER BY ah."LEVEL"
) h ON hp.HIERARCHYID = h.HIERARCHYID
ORDER BY h."LEVEL", hp.PERMISSIONID

View File

@@ -0,0 +1,22 @@
SELECT
hr.HIERARCHYID AS HIERARCHYID,
h."LEVEL" AS "LEVEL",
hr.ROLEID AS ROLEID,
rp.PERMISSIONID AS PERMISSIONID,
p.STRID AS PERMISSION_STRID,
p.SETTINGS AS SETTINGS,
rp.PDATA AS PDATA,
p.COMMAND AS COMMAND,
p.HTTP AS HTTP
FROM HIERARCHYROLE hr
JOIN (
SELECT ah.HIERARCHYID, ah."LEVEL"
FROM AUTHHIERARCHY ah, AUTHHIERARCHY ah2
WHERE ah.LEFTKEY <= ah2.LEFTKEY
AND ah.RIGHTKEY >= ah2.RIGHTKEY
AND ah2.HIERARCHYID = :hierarchyId
ORDER BY ah."LEVEL"
) h ON hr.HIERARCHYID = h.HIERARCHYID
JOIN ROLEPERMISSIONS rp ON rp.ROLEID = hr.ROLEID
JOIN PERMISSIONS p ON p.PERMISSIONID = rp.PERMISSIONID
ORDER BY h."LEVEL", hr.ROLEID, rp.PERMISSIONID

View File

@@ -0,0 +1,3 @@
SELECT KEY, VALUE
FROM PRIVATEDATA
WHERE AUTHID = :authId

View File

@@ -0,0 +1,11 @@
SELECT r.SETTINGS
FROM (
SELECT ah.HIERARCHYID, ah."LEVEL"
FROM AUTHHIERARCHY ah, AUTHHIERARCHY ah2
WHERE ah.LEFTKEY <= ah2.LEFTKEY
AND ah.RIGHTKEY >= ah2.RIGHTKEY
AND ah2.AUTHID = :authId
) h
JOIN HIERARCHYROLE hr ON h.HIERARCHYID = hr.HIERARCHYID
JOIN ROLES r ON r.ROLEID = hr.ROLEID
ORDER BY h."LEVEL"

View File

@@ -0,0 +1,13 @@
SELECT
ad.AUTHID AS AD_AUTHID,
ad.DATAID AS AD_DATAID,
ad.TYPE AS AD_TYPE,
ad.DATA AS AD_DATA,
ai.PARAMETERS AS AI_PARAMETERS,
ah.HIERARCHYID AS AH_HIERARCHYID,
ah.DISPLAYNAME AS AH_DISPLAYNAME
FROM AUTHDATA ad
LEFT JOIN AUTHIDS ai ON ai.AUTHID = ad.AUTHID
LEFT JOIN AUTHHIERARCHY ah ON ah.AUTHID = ad.AUTHID
WHERE ad.DATAID = :dataId
AND ad.TYPE = :type