forked from baron/baron-sso
consent 변화 내부 머지 완료
This commit is contained in:
@@ -159,6 +159,20 @@ func main() {
|
||||
slog.Info("✅ Connected to ClickHouse")
|
||||
}
|
||||
|
||||
var oathkeeperRepo domain.OathkeeperLogRepository
|
||||
oryCHHost := getEnv("ORY_CLICKHOUSE_HOST", "ory_clickhouse")
|
||||
oryCHPort, _ := strconv.Atoi(getEnv("ORY_CLICKHOUSE_PORT_NATIVE", "9000"))
|
||||
oryCHUser := getEnv("ORY_CLICKHOUSE_USER", "ory")
|
||||
oryCHPass := getEnv("ORY_CLICKHOUSE_PASSWORD", "orypass")
|
||||
oryCHDB := getEnv("ORY_CLICKHOUSE_DB", "ory")
|
||||
if repo, err := repository.NewOathkeeperClickHouseRepository(oryCHHost, oryCHPort, oryCHUser, oryCHPass, oryCHDB); err != nil {
|
||||
slog.Warn("Failed to connect to Ory ClickHouse. Oathkeeper logs will be skipped.", "error", err)
|
||||
oathkeeperRepo = nil
|
||||
} else {
|
||||
oathkeeperRepo = repo
|
||||
slog.Info("✅ Connected to Ory ClickHouse")
|
||||
}
|
||||
|
||||
// PostgreSQL (Meta Store)
|
||||
pgHost := getEnv("DB_HOST", "localhost")
|
||||
pgPort := getEnv("DB_PORT", "5432")
|
||||
@@ -228,7 +242,7 @@ func main() {
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
|
||||
auditHandler := handler.NewAuditHandler(auditRepo)
|
||||
authHandler := handler.NewAuthHandler(redisService, idpProvider, auditRepo, tenantService, userRepo)
|
||||
authHandler := handler.NewAuthHandler(redisService, idpProvider, auditRepo, oathkeeperRepo, tenantService, userRepo)
|
||||
adminHandler := handler.NewAdminHandler()
|
||||
devHandler := handler.NewDevHandler(redisService)
|
||||
tenantHandler := handler.NewTenantHandler(db, tenantService)
|
||||
|
||||
@@ -80,17 +80,18 @@ const (
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
ProjectID string
|
||||
SmsService domain.SmsService
|
||||
EmailService domain.EmailService
|
||||
RedisService *service.RedisService
|
||||
DescopeClient *client.DescopeClient
|
||||
KratosAdmin *service.KratosAdminService
|
||||
IdpProvider domain.IdentityProvider
|
||||
AuditRepo domain.AuditRepository
|
||||
Hydra *service.HydraAdminService
|
||||
TenantService service.TenantService
|
||||
UserRepo repository.UserRepository
|
||||
ProjectID string
|
||||
SmsService domain.SmsService
|
||||
EmailService domain.EmailService
|
||||
RedisService *service.RedisService
|
||||
DescopeClient *client.DescopeClient
|
||||
KratosAdmin *service.KratosAdminService
|
||||
IdpProvider domain.IdentityProvider
|
||||
AuditRepo domain.AuditRepository
|
||||
OathkeeperRepo domain.OathkeeperLogRepository
|
||||
Hydra *service.HydraAdminService
|
||||
TenantService service.TenantService
|
||||
UserRepo repository.UserRepository
|
||||
}
|
||||
|
||||
type signupState struct {
|
||||
@@ -148,7 +149,7 @@ func checkPollInterval(redis *service.RedisService, key string, interval time.Du
|
||||
return false, int(interval.Seconds())
|
||||
}
|
||||
|
||||
func NewAuthHandler(redisService *service.RedisService, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, tenantService service.TenantService, userRepo repository.UserRepository) *AuthHandler {
|
||||
func NewAuthHandler(redisService *service.RedisService, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, oathkeeperRepo domain.OathkeeperLogRepository, tenantService service.TenantService, userRepo repository.UserRepository) *AuthHandler {
|
||||
projectID := os.Getenv("DESCOPE_PROJECT_ID")
|
||||
managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY")
|
||||
|
||||
@@ -165,17 +166,18 @@ func NewAuthHandler(redisService *service.RedisService, idpProvider domain.Ident
|
||||
}
|
||||
|
||||
return &AuthHandler{
|
||||
ProjectID: projectID,
|
||||
SmsService: service.NewSmsService(),
|
||||
EmailService: service.NewEmailService(),
|
||||
RedisService: redisService,
|
||||
DescopeClient: descopeClient,
|
||||
KratosAdmin: service.NewKratosAdminService(),
|
||||
IdpProvider: idpProvider,
|
||||
AuditRepo: auditRepo,
|
||||
Hydra: service.NewHydraAdminService(),
|
||||
TenantService: tenantService,
|
||||
UserRepo: userRepo,
|
||||
ProjectID: projectID,
|
||||
SmsService: service.NewSmsService(),
|
||||
EmailService: service.NewEmailService(),
|
||||
RedisService: redisService,
|
||||
DescopeClient: descopeClient,
|
||||
KratosAdmin: service.NewKratosAdminService(),
|
||||
IdpProvider: idpProvider,
|
||||
AuditRepo: auditRepo,
|
||||
OathkeeperRepo: oathkeeperRepo,
|
||||
Hydra: service.NewHydraAdminService(),
|
||||
TenantService: tenantService,
|
||||
UserRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1155,8 +1157,8 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
|
||||
// VerifyLoginShortCode - Verify short code (2 letters + 6 digits) and issue/approve session.
|
||||
func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
ShortCode string `json:"shortCode"`
|
||||
VerifyOnly bool `json:"verifyOnly,omitempty"`
|
||||
ShortCode string `json:"shortCode"`
|
||||
VerifyOnly bool `json:"verifyOnly,omitempty"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
slog.Error("[LoginShortCode] Body parse error", "error", err)
|
||||
@@ -2700,8 +2702,31 @@ func extractClientIPFromHeaders(c *fiber.Ctx) string {
|
||||
return c.IP()
|
||||
}
|
||||
|
||||
type authTimelineItem struct {
|
||||
EventID string `json:"event_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
UserID string `json:"user_id"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
EventType string `json:"event_type"`
|
||||
Status string `json:"status"`
|
||||
AuthMethod string `json:"auth_method,omitempty"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
AppName string `json:"app_name,omitempty"`
|
||||
ParentSessionID string `json:"parent_session_id,omitempty"`
|
||||
}
|
||||
|
||||
type consentClientInfo struct {
|
||||
ClientID string
|
||||
Name string
|
||||
ConsentAt time.Time
|
||||
}
|
||||
|
||||
func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
||||
if h.AuditRepo == nil {
|
||||
if h.AuditRepo == nil && h.OathkeeperRepo == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Audit service unavailable"})
|
||||
}
|
||||
|
||||
@@ -2728,6 +2753,51 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
|
||||
}
|
||||
|
||||
subject := ""
|
||||
if h.OathkeeperRepo != nil {
|
||||
if value, err := h.resolveConsentSubject(c); err == nil {
|
||||
subject = value
|
||||
}
|
||||
}
|
||||
|
||||
consentMap := make(map[string]consentClientInfo)
|
||||
if subject != "" && h.Hydra != nil {
|
||||
if sessions, err := h.Hydra.ListConsentSessions(c.Context(), subject, ""); err == nil {
|
||||
for _, session := range sessions {
|
||||
clientID := strings.TrimSpace(session.Client.ClientID)
|
||||
if clientID == "" {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(session.Client.ClientName)
|
||||
if name == "" {
|
||||
name = clientID
|
||||
}
|
||||
consentAt := time.Time{}
|
||||
if session.AuthenticatedAt != nil {
|
||||
consentAt = *session.AuthenticatedAt
|
||||
} else if session.RequestedAt != nil {
|
||||
consentAt = *session.RequestedAt
|
||||
}
|
||||
if existing, ok := consentMap[clientID]; ok {
|
||||
if !consentAt.IsZero() && (existing.ConsentAt.IsZero() || consentAt.Before(existing.ConsentAt)) {
|
||||
existing.ConsentAt = consentAt
|
||||
consentMap[clientID] = existing
|
||||
}
|
||||
if existing.Name == "" {
|
||||
existing.Name = name
|
||||
consentMap[clientID] = existing
|
||||
}
|
||||
continue
|
||||
}
|
||||
consentMap[clientID] = consentClientInfo{
|
||||
ClientID: clientID,
|
||||
Name: name,
|
||||
ConsentAt: consentAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates := buildLoginCandidates(profile)
|
||||
fetchLimit := limit * 10
|
||||
if fetchLimit < limit {
|
||||
@@ -2737,69 +2807,177 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
||||
fetchLimit = 500
|
||||
}
|
||||
|
||||
items := make([]domain.AuditLog, 0, limit)
|
||||
nextCursor := ""
|
||||
currentCursor := cursor
|
||||
const maxBatches = 10
|
||||
for batch := 0; batch < maxBatches && len(items) < limit; batch++ {
|
||||
logs, err := h.AuditRepo.FindPage(c.Context(), fetchLimit, currentCursor)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to retrieve audit logs"})
|
||||
}
|
||||
if len(logs) == 0 {
|
||||
nextCursor = ""
|
||||
break
|
||||
}
|
||||
|
||||
var lastScanned *domain.AuditLog
|
||||
for i := range logs {
|
||||
log := logs[i]
|
||||
lastScanned = &log
|
||||
if !isAuthEventType(log.EventType) {
|
||||
continue
|
||||
authLogs := make([]domain.AuditLog, 0, fetchLimit)
|
||||
if h.AuditRepo != nil {
|
||||
currentCursor := cursor
|
||||
const maxBatches = 10
|
||||
for batch := 0; batch < maxBatches && len(authLogs) < fetchLimit; batch++ {
|
||||
logs, err := h.AuditRepo.FindPage(c.Context(), fetchLimit, currentCursor)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to retrieve audit logs"})
|
||||
}
|
||||
if !matchesAuthTimelineUser(log, profile, candidates) {
|
||||
continue
|
||||
}
|
||||
if shouldSkipAuthTimeline(log) {
|
||||
continue
|
||||
}
|
||||
if log.UserID == "" {
|
||||
log.UserID = profile.ID
|
||||
}
|
||||
log.AuthMethod = deriveAuthMethod(log)
|
||||
if log.AuthMethod == "" {
|
||||
continue
|
||||
}
|
||||
if log.SessionID == "" {
|
||||
log.SessionID = extractSessionIDFromAuditDetails(log.Details)
|
||||
}
|
||||
items = append(items, log)
|
||||
if len(items) >= limit {
|
||||
nextCursor = encodeAuditCursor(log)
|
||||
if len(logs) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(items) >= limit {
|
||||
break
|
||||
}
|
||||
var lastScanned *domain.AuditLog
|
||||
for i := range logs {
|
||||
log := logs[i]
|
||||
lastScanned = &log
|
||||
if !isAuthEventType(log.EventType) {
|
||||
continue
|
||||
}
|
||||
if !matchesAuthTimelineUser(log, profile, candidates) {
|
||||
continue
|
||||
}
|
||||
if shouldSkipAuthTimeline(log) {
|
||||
continue
|
||||
}
|
||||
if log.UserID == "" {
|
||||
log.UserID = profile.ID
|
||||
}
|
||||
log.AuthMethod = deriveAuthMethod(log)
|
||||
if log.AuthMethod == "" {
|
||||
continue
|
||||
}
|
||||
if log.SessionID == "" {
|
||||
log.SessionID = extractSessionIDFromAuditDetails(log.Details)
|
||||
}
|
||||
authLogs = append(authLogs, log)
|
||||
if len(authLogs) >= fetchLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(logs) < fetchLimit {
|
||||
nextCursor = ""
|
||||
break
|
||||
if len(logs) < fetchLimit || lastScanned == nil {
|
||||
break
|
||||
}
|
||||
currentCursor = &domain.AuditCursor{
|
||||
Timestamp: lastScanned.Timestamp,
|
||||
EventID: lastScanned.EventID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lastScanned == nil {
|
||||
nextCursor = ""
|
||||
break
|
||||
}
|
||||
oathkeeperLogs := make([]domain.OathkeeperAccessLog, 0, fetchLimit)
|
||||
if h.OathkeeperRepo != nil && subject != "" {
|
||||
currentCursor := cursor
|
||||
const maxBatches = 10
|
||||
for batch := 0; batch < maxBatches && len(oathkeeperLogs) < fetchLimit; batch++ {
|
||||
logs, err := h.OathkeeperRepo.FindPageBySubject(c.Context(), subject, fetchLimit, currentCursor)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to retrieve oathkeeper logs"})
|
||||
}
|
||||
if len(logs) == 0 {
|
||||
break
|
||||
}
|
||||
var lastScanned *domain.OathkeeperAccessLog
|
||||
for i := range logs {
|
||||
log := logs[i]
|
||||
lastScanned = &log
|
||||
clientID := extractClientIDFromOathkeeperLog(log)
|
||||
if clientID == "" {
|
||||
continue
|
||||
}
|
||||
consent, ok := consentMap[clientID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !consent.ConsentAt.IsZero() && log.Timestamp.Before(consent.ConsentAt) {
|
||||
continue
|
||||
}
|
||||
oathkeeperLogs = append(oathkeeperLogs, log)
|
||||
if len(oathkeeperLogs) >= fetchLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
currentCursor = &domain.AuditCursor{
|
||||
Timestamp: lastScanned.Timestamp,
|
||||
EventID: lastScanned.EventID,
|
||||
if len(logs) < fetchLimit || lastScanned == nil {
|
||||
break
|
||||
}
|
||||
currentCursor = &domain.AuditCursor{
|
||||
Timestamp: lastScanned.Timestamp,
|
||||
EventID: oathkeeperEventID(*lastScanned),
|
||||
}
|
||||
}
|
||||
nextCursor = encodeAuditCursor(*lastScanned)
|
||||
}
|
||||
|
||||
items := make([]authTimelineItem, 0, len(authLogs)+len(oathkeeperLogs))
|
||||
for i := range authLogs {
|
||||
log := authLogs[i]
|
||||
item := authTimelineItem{
|
||||
EventID: log.EventID,
|
||||
Timestamp: log.Timestamp,
|
||||
UserID: log.UserID,
|
||||
SessionID: log.SessionID,
|
||||
EventType: log.EventType,
|
||||
Status: log.Status,
|
||||
AuthMethod: log.AuthMethod,
|
||||
IPAddress: log.IPAddress,
|
||||
UserAgent: log.UserAgent,
|
||||
Details: log.Details,
|
||||
Source: "backend",
|
||||
AppName: "Baron 통합로그인",
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
for i := range oathkeeperLogs {
|
||||
log := oathkeeperLogs[i]
|
||||
clientID := extractClientIDFromOathkeeperLog(log)
|
||||
if clientID == "" {
|
||||
continue
|
||||
}
|
||||
consent := consentMap[clientID]
|
||||
appName := consent.Name
|
||||
if appName == "" {
|
||||
appName = clientID
|
||||
}
|
||||
details := map[string]any{
|
||||
"path": log.Path,
|
||||
"client_id": clientID,
|
||||
"decision": log.Decision,
|
||||
"status_code": log.Status,
|
||||
}
|
||||
detailsJSON, _ := json.Marshal(details)
|
||||
status := "success"
|
||||
if log.Status >= 400 {
|
||||
status = "failure"
|
||||
}
|
||||
eventID := oathkeeperEventID(log)
|
||||
item := authTimelineItem{
|
||||
EventID: eventID,
|
||||
Timestamp: log.Timestamp,
|
||||
UserID: profile.ID,
|
||||
EventType: fmt.Sprintf("%s %s", log.Method, log.Path),
|
||||
Status: status,
|
||||
AuthMethod: "세션 위임",
|
||||
IPAddress: log.ClientIP,
|
||||
UserAgent: log.UserAgent,
|
||||
Details: string(detailsJSON),
|
||||
Source: "oathkeeper",
|
||||
ClientID: clientID,
|
||||
AppName: appName,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].Timestamp.Equal(items[j].Timestamp) {
|
||||
return items[i].EventID > items[j].EventID
|
||||
}
|
||||
return items[i].Timestamp.After(items[j].Timestamp)
|
||||
})
|
||||
|
||||
nextCursor := ""
|
||||
hasMore := len(authLogs) >= fetchLimit || len(oathkeeperLogs) >= fetchLimit
|
||||
if len(items) > limit {
|
||||
items = items[:limit]
|
||||
last := items[len(items)-1]
|
||||
nextCursor = encodeTimelineCursor(last.Timestamp, last.EventID)
|
||||
} else if hasMore && len(items) > 0 {
|
||||
last := items[len(items)-1]
|
||||
nextCursor = encodeTimelineCursor(last.Timestamp, last.EventID)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
@@ -2810,6 +2988,27 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func encodeTimelineCursor(timestamp time.Time, eventID string) string {
|
||||
if eventID == "" {
|
||||
eventID = fmt.Sprintf("%d", timestamp.UnixNano())
|
||||
}
|
||||
payload := timestamp.UTC().Format(time.RFC3339Nano) + "|" + eventID
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(payload))
|
||||
}
|
||||
|
||||
func oathkeeperEventID(log domain.OathkeeperAccessLog) string {
|
||||
if log.RequestID != "" {
|
||||
return log.RequestID
|
||||
}
|
||||
if log.TraceID != "" {
|
||||
return log.TraceID
|
||||
}
|
||||
if log.SpanID != "" {
|
||||
return log.SpanID
|
||||
}
|
||||
return fmt.Sprintf("%d", log.Timestamp.UnixNano())
|
||||
}
|
||||
|
||||
type linkedRpSummary struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -3365,6 +3564,80 @@ func extractLoginIDFromAuditDetails(details string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractClientIDFromOathkeeperLog(log domain.OathkeeperAccessLog) string {
|
||||
if value := strings.TrimSpace(log.RP); value != "" {
|
||||
return value
|
||||
}
|
||||
if value := parseClientIDFromURL(log.Target); value != "" {
|
||||
return value
|
||||
}
|
||||
if value := parseClientIDFromURL(log.Path); value != "" {
|
||||
return value
|
||||
}
|
||||
return parseClientIDFromRaw(log.Raw)
|
||||
}
|
||||
|
||||
func parseClientIDFromURL(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if id := strings.TrimSpace(parsed.Query().Get("client_id")); id != "" {
|
||||
return id
|
||||
}
|
||||
if id := strings.TrimSpace(parsed.Query().Get("clientId")); id != "" {
|
||||
return id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseClientIDFromRaw(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
if id := readClientIDFromPayload(payload); id != "" {
|
||||
return id
|
||||
}
|
||||
if request, ok := payload["request"].(map[string]any); ok {
|
||||
if id := readClientIDFromPayload(request); id != "" {
|
||||
return id
|
||||
}
|
||||
if urlRaw, ok := request["url"].(string); ok {
|
||||
if id := parseClientIDFromURL(urlRaw); id != "" {
|
||||
return id
|
||||
}
|
||||
}
|
||||
if pathRaw, ok := request["path"].(string); ok {
|
||||
if id := parseClientIDFromURL(pathRaw); id != "" {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func readClientIDFromPayload(payload map[string]any) string {
|
||||
if payload == nil {
|
||||
return ""
|
||||
}
|
||||
if raw, ok := payload["client_id"].(string); ok && strings.TrimSpace(raw) != "" {
|
||||
return strings.TrimSpace(raw)
|
||||
}
|
||||
if raw, ok := payload["clientId"].(string); ok && strings.TrimSpace(raw) != "" {
|
||||
return strings.TrimSpace(raw)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractSessionIDFromAuditDetails(details string) string {
|
||||
if details == "" {
|
||||
return ""
|
||||
|
||||
@@ -7,13 +7,23 @@ CREATE TABLE IF NOT EXISTS ory.oathkeeper_access_logs (
|
||||
path String DEFAULT '',
|
||||
status UInt16 DEFAULT 0,
|
||||
latency_ms UInt32 DEFAULT 0,
|
||||
client_id String DEFAULT '',
|
||||
rp String DEFAULT '',
|
||||
action String DEFAULT '',
|
||||
target String DEFAULT '',
|
||||
rule_id String DEFAULT '',
|
||||
host String DEFAULT '',
|
||||
scheme String DEFAULT '',
|
||||
query String DEFAULT '',
|
||||
upstream_url String DEFAULT '',
|
||||
subject String DEFAULT '',
|
||||
parent_session_id String DEFAULT '',
|
||||
client_ip String DEFAULT '',
|
||||
user_agent String DEFAULT '',
|
||||
referer String DEFAULT '',
|
||||
decision String DEFAULT '',
|
||||
bytes_in UInt64 DEFAULT 0,
|
||||
bytes_out UInt64 DEFAULT 0,
|
||||
trace_id String DEFAULT '',
|
||||
span_id String DEFAULT '',
|
||||
raw String DEFAULT ''
|
||||
|
||||
@@ -84,5 +84,33 @@
|
||||
"authenticators": [{ "handler": "noop" }],
|
||||
"authorizer": { "handler": "allow" },
|
||||
"mutators": [{ "handler": "noop" }]
|
||||
},
|
||||
{
|
||||
"id": "rp-template-browser",
|
||||
"description": "RP proxy (browser session). TODO: match.url/upstream.url을 실제 RP로 좁혀야 함.",
|
||||
"match": {
|
||||
"url": "http://<.*>/rp/<.*>",
|
||||
"methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
||||
},
|
||||
"upstream": {
|
||||
"url": "http://rp_upstream:8080"
|
||||
},
|
||||
"authenticators": [{ "handler": "cookie_session" }],
|
||||
"authorizer": { "handler": "allow" },
|
||||
"mutators": [{ "handler": "noop" }]
|
||||
},
|
||||
{
|
||||
"id": "rp-template-bearer",
|
||||
"description": "RP proxy (bearer). TODO: oauth2_introspection 또는 jwt 활성화 필요.",
|
||||
"match": {
|
||||
"url": "http://<.*>/rp-api/<.*>",
|
||||
"methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
||||
},
|
||||
"upstream": {
|
||||
"url": "http://rp_upstream:8080"
|
||||
},
|
||||
"authenticators": [{ "handler": "oauth2_introspection" }],
|
||||
"authorizer": { "handler": "allow" },
|
||||
"mutators": [{ "handler": "noop" }]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
request_method = get(parsed, ["request", "method"]) ?? ""
|
||||
request_path = get(parsed, ["request", "path"]) ?? ""
|
||||
request_url = get(parsed, ["request", "url"]) ?? ""
|
||||
request_host = get(parsed, ["request", "host"]) ?? ""
|
||||
request_scheme = get(parsed, ["request", "scheme"]) ?? ""
|
||||
request_query = get(parsed, ["request", "query"]) ?? ""
|
||||
.method = parsed.method ?? parsed.http_method ?? request_method ?? ""
|
||||
.path = parsed.path ?? parsed.http_path ?? request_path ?? request_url ?? ""
|
||||
response_status = get(parsed, ["response", "status"]) ?? 0
|
||||
@@ -27,6 +30,7 @@
|
||||
.user_agent = parsed.user_agent
|
||||
if is_null(.user_agent) { .user_agent = get(headers, ["User-Agent"]) }
|
||||
if is_null(.user_agent) { .user_agent = "" }
|
||||
.referer = get(headers, ["Referer"]) ?? ""
|
||||
|
||||
.decision = parsed.decision
|
||||
if is_null(.decision) { .decision = parsed.result }
|
||||
@@ -38,9 +42,18 @@
|
||||
.span_id = parsed.span_id
|
||||
if is_null(.span_id) { .span_id = "" }
|
||||
|
||||
.rp = ""
|
||||
.action = ""
|
||||
.target = ""
|
||||
.rp = parsed.rp ?? ""
|
||||
.action = parsed.action ?? ""
|
||||
.target = parsed.target ?? ""
|
||||
.rule_id = parsed.rule_id ?? get(parsed, ["rule", "id"]) ?? ""
|
||||
.client_id = parsed.client_id ?? get(parsed, ["client", "id"]) ?? ""
|
||||
.parent_session_id = parsed.parent_session_id ?? get(parsed, ["extra", "parent_session_id"]) ?? ""
|
||||
.host = parsed.host ?? request_host ?? ""
|
||||
.scheme = parsed.scheme ?? request_scheme ?? ""
|
||||
.query = parsed.query ?? request_query ?? ""
|
||||
.upstream_url = parsed.upstream_url ?? get(parsed, ["upstream", "url"]) ?? ""
|
||||
.bytes_in = to_int(parsed.bytes_in ?? parsed.request_bytes ?? 0) ?? 0
|
||||
.bytes_out = to_int(parsed.bytes_out ?? parsed.response_bytes ?? 0) ?? 0
|
||||
'''
|
||||
|
||||
[sinks.clickhouse]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/constants/error_whitelist.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
|
||||
class ErrorScreen extends StatelessWidget {
|
||||
final String? errorId;
|
||||
@@ -16,15 +18,22 @@ class ErrorScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final errorType = (errorCode == null || errorCode!.isEmpty)
|
||||
? 'unknown_error'
|
||||
: errorCode!;
|
||||
final title = errorCode == null || errorCode!.isEmpty
|
||||
final isProd = AuthProxyService.isProdEnv;
|
||||
final normalizedCode = (errorCode ?? '').trim();
|
||||
final hasCode = normalizedCode.isNotEmpty;
|
||||
final whitelistMessage = errorWhitelistMessages[normalizedCode];
|
||||
final isWhitelisted = whitelistMessage != null;
|
||||
final errorType = isProd
|
||||
? (isWhitelisted && hasCode ? normalizedCode : 'unknown_error')
|
||||
: (hasCode ? normalizedCode : 'unknown_error');
|
||||
final title = isProd
|
||||
? '인증 과정에서 오류가 발생했습니다'
|
||||
: '오류: $errorCode';
|
||||
final detail = description?.isNotEmpty == true
|
||||
? description!
|
||||
: '요청을 처리하는 중 문제가 발생했습니다. 잠시 후 다시 시도해 주세요.';
|
||||
: (hasCode ? '오류: $normalizedCode' : '오류가 발생했습니다');
|
||||
final detail = isProd
|
||||
? (isWhitelisted ? whitelistMessage! : '에러가 계속되면 관리자에게 문의해주세요')
|
||||
: ((description?.isNotEmpty == true)
|
||||
? description!
|
||||
: (hasCode ? '오류가 발생했습니다.' : '요청을 처리하는 중 문제가 발생했습니다.'));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F8FA),
|
||||
|
||||
@@ -21,6 +21,10 @@ class AuditLogEntry {
|
||||
final String userAgent;
|
||||
final String sessionId;
|
||||
final String details;
|
||||
final String source;
|
||||
final String clientId;
|
||||
final String appName;
|
||||
final String parentSessionId;
|
||||
|
||||
AuditLogEntry({
|
||||
required this.eventId,
|
||||
@@ -33,6 +37,10 @@ class AuditLogEntry {
|
||||
required this.userAgent,
|
||||
required this.sessionId,
|
||||
required this.details,
|
||||
required this.source,
|
||||
required this.clientId,
|
||||
required this.appName,
|
||||
required this.parentSessionId,
|
||||
});
|
||||
|
||||
factory AuditLogEntry.fromJson(Map<String, dynamic> json) {
|
||||
@@ -55,6 +63,10 @@ class AuditLogEntry {
|
||||
userAgent: json['user_agent'] ?? '',
|
||||
sessionId: json['session_id'] ?? '',
|
||||
details: json['details'] ?? '',
|
||||
source: json['source'] ?? '',
|
||||
clientId: json['client_id'] ?? '',
|
||||
appName: json['app_name'] ?? '',
|
||||
parentSessionId: json['parent_session_id'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -542,6 +554,34 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String _appLabelForLog(AuditLogEntry log) {
|
||||
if (log.appName.isNotEmpty) {
|
||||
return log.appName;
|
||||
}
|
||||
return _appLabelForPath(log.path);
|
||||
}
|
||||
|
||||
Widget _buildAppCell(AuditLogEntry log, {TextStyle? style}) {
|
||||
final label = _appLabelForLog(log);
|
||||
if (label == 'Baron 통합로그인') {
|
||||
return _selectableText(label, style: style);
|
||||
}
|
||||
final tooltip = log.parentSessionId.isEmpty
|
||||
? '부모 세션 ID 없음'
|
||||
: '부모 세션 ID: ${log.parentSessionId}';
|
||||
final baseStyle = style ?? const TextStyle();
|
||||
final emphasisStyle = log.parentSessionId.isEmpty
|
||||
? baseStyle
|
||||
: baseStyle.copyWith(
|
||||
color: Colors.blueAccent,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: _selectableText(label, style: emphasisStyle),
|
||||
);
|
||||
}
|
||||
|
||||
String _appLabelForPath(String path) {
|
||||
if (path.startsWith('/api/v1/auth')) {
|
||||
return 'Baron 통합로그인';
|
||||
@@ -992,13 +1032,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
rows: logs.map((log) {
|
||||
final statusLabel = log.status == 'success' ? '성공' : '실패';
|
||||
final statusColor = log.status == 'success' ? Colors.green : Colors.redAccent;
|
||||
final appLabel = _appLabelForPath(log.path);
|
||||
final authMethod = log.authMethod.isNotEmpty ? log.authMethod : _authMethodLabel();
|
||||
final deviceLabel = _deviceLabelFromUserAgent(log.userAgent);
|
||||
return DataRow(cells: [
|
||||
DataCell(_selectableText(log.sessionId.isEmpty ? '-' : log.sessionId)),
|
||||
DataCell(_selectableText(_formatDateTime(log.timestamp))),
|
||||
DataCell(_selectableText(appLabel)),
|
||||
DataCell(_buildAppCell(log)),
|
||||
DataCell(_selectableText(log.ipAddress.isEmpty ? '-' : log.ipAddress)),
|
||||
DataCell(_selectableText(deviceLabel)),
|
||||
DataCell(_buildAuthMethodCell(log, authMethod)),
|
||||
@@ -1036,8 +1075,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _selectableText(
|
||||
_appLabelForPath(log.path),
|
||||
child: _buildAppCell(
|
||||
log,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: _ink),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user