forked from baron/baron-sso
consent 변화 내부 머지 완료
This commit is contained in:
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user