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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 55 additions & 17 deletions core/api/analytics.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
// (sentinel:all). Mirrors the GetApplicationSecret gate.
func requireAnalyticsAccess(c *gin.Context) {
Require(c, Any(
RequestTokenHasScope(c, "sentinel:all"),
RequestTokenHasInternalAccess(c),
RequestTokenHasAudience(c, "sentinel") && RequestUserIsAdmin(c),
))
}
Expand All @@ -33,15 +33,17 @@ func recordAudit(c *gin.Context, action model.AuditAction, targetType string, ta
})
}

// queryInt reads an integer query param, falling back to def when absent or
// unparseable.
func queryInt(c *gin.Context, key string, def int) int {
if v := c.Query(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
func boundedQueryInt(c *gin.Context, key string, fallback int, maximum int) (int, bool) {
raw := c.Query(key)
if raw == "" {
return fallback, true
}
return def
value, err := strconv.Atoi(raw)
if err != nil || value < 1 || value > maximum {
c.JSON(http.StatusBadRequest, gin.H{"error": key + " must be between 1 and " + strconv.Itoa(maximum)})
return 0, false
}
return value, true
}

func AnalyticsOverview(c *gin.Context) {
Expand All @@ -56,7 +58,11 @@ func AnalyticsOverview(c *gin.Context) {

func AnalyticsLoginTimeSeries(c *gin.Context) {
requireAnalyticsAccess(c)
series, err := service.GetLoginTimeSeries(queryInt(c, "days", 30))
days, ok := boundedQueryInt(c, "days", 30, service.MaxAnalyticsDays)
if !ok {
return
}
series, err := service.GetLoginTimeSeries(days)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
Expand All @@ -66,7 +72,11 @@ func AnalyticsLoginTimeSeries(c *gin.Context) {

func AnalyticsLoginHeatmap(c *gin.Context) {
requireAnalyticsAccess(c)
cells, err := service.GetLoginHeatmap(queryInt(c, "days", 90))
days, ok := boundedQueryInt(c, "days", 90, service.MaxAnalyticsDays)
if !ok {
return
}
cells, err := service.GetLoginHeatmap(days)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
Expand All @@ -76,7 +86,15 @@ func AnalyticsLoginHeatmap(c *gin.Context) {

func AnalyticsTopApplications(c *gin.Context) {
requireAnalyticsAccess(c)
apps, err := service.GetTopApplications(queryInt(c, "days", 30), queryInt(c, "limit", 10))
days, ok := boundedQueryInt(c, "days", 30, service.MaxAnalyticsDays)
if !ok {
return
}
limit, ok := boundedQueryInt(c, "limit", 10, service.MaxAnalyticsLimit)
if !ok {
return
}
apps, err := service.GetTopApplications(days, limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
Expand All @@ -86,7 +104,11 @@ func AnalyticsTopApplications(c *gin.Context) {

func AnalyticsUserGrowth(c *gin.Context) {
requireAnalyticsAccess(c)
growth, err := service.GetUserGrowth(queryInt(c, "months", 12))
months, ok := boundedQueryInt(c, "months", 12, service.MaxAnalyticsMonths)
if !ok {
return
}
growth, err := service.GetUserGrowth(months)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
Expand All @@ -96,7 +118,11 @@ func AnalyticsUserGrowth(c *gin.Context) {

func AnalyticsMemberDemographics(c *gin.Context) {
requireAnalyticsAccess(c)
demographics, err := service.GetMemberDemographics(queryInt(c, "major_limit", 10))
limit, ok := boundedQueryInt(c, "major_limit", 10, service.MaxAnalyticsLimit)
if !ok {
return
}
demographics, err := service.GetMemberDemographics(limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
Expand Down Expand Up @@ -126,7 +152,11 @@ func AnalyticsGroupMembership(c *gin.Context) {

func AnalyticsJoinRequests(c *gin.Context) {
requireAnalyticsAccess(c)
funnel, err := service.GetJoinRequestFunnel(queryInt(c, "days", 90))
days, ok := boundedQueryInt(c, "days", 90, service.MaxAnalyticsDays)
if !ok {
return
}
funnel, err := service.GetJoinRequestFunnel(days)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
Expand All @@ -136,14 +166,18 @@ func AnalyticsJoinRequests(c *gin.Context) {

func AnalyticsAuditEvents(c *gin.Context) {
requireAnalyticsAccess(c)
limit, ok := boundedQueryInt(c, "limit", 100, service.MaxAuditEventLimit)
if !ok {
return
}
events, err := service.GetAuditEvents(service.AuditEventsFilter{
ActorID: c.Query("actor_id"),
Action: c.Query("action"),
TargetType: c.Query("target_type"),
TargetID: c.Query("target_id"),
Before: c.Query("before"),
After: c.Query("after"),
Limit: c.Query("limit"),
Limit: strconv.Itoa(limit),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
Expand All @@ -154,7 +188,11 @@ func AnalyticsAuditEvents(c *gin.Context) {

func AnalyticsAuditSummary(c *gin.Context) {
requireAnalyticsAccess(c)
summary, err := service.GetAuditActionSummary(queryInt(c, "days", 30))
days, ok := boundedQueryInt(c, "days", 30, service.MaxAnalyticsDays)
if !ok {
return
}
summary, err := service.GetAuditActionSummary(days)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
Expand Down
6 changes: 3 additions & 3 deletions core/model/entity_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import "time"

type EntityLogin struct {
ID string `json:"id" gorm:"primaryKey"`
EntityID string `json:"entity_id" gorm:"index"`
ClientID string `json:"client_id" gorm:"index"`
EntityID string `json:"entity_id" gorm:"index;index:idx_entity_login_entity_created,priority:1"`
ClientID string `json:"client_id" gorm:"index;index:idx_entity_login_client_created,priority:1"`
Scope string `json:"scope"`
AccessTokenID string `json:"access_token_id"`
RefreshTokenID string `json:"refresh_token_id"`
IPAddress string `json:"ip_address"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_entity_login_created_at;index:idx_entity_login_entity_created,priority:2;index:idx_entity_login_client_created,priority:2"`
}

func (EntityLogin) TableName() string {
Expand Down
5 changes: 2 additions & 3 deletions core/model/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ type GroupJoinRequest struct {
ID string `json:"id" gorm:"primaryKey"`
GroupID string `json:"group_id"`
EntityID string `json:"entity_id"`
Status string `json:"status"`
Status string `json:"status" gorm:"index;index:idx_group_join_request_status_created,priority:1"`
ReviewedBy string `json:"reviewed_by"`
ReviewedAt time.Time `json:"reviewed_at"`
HasExpiration bool `json:"has_expiration"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_group_join_request_status_created,priority:2"`
Comments []GroupJoinRequestComment `json:"comments" gorm:"-"`
}

Expand All @@ -112,4 +112,3 @@ type GroupJoinRequestComment struct {
func (GroupJoinRequestComment) TableName() string {
return "group_join_request_comment"
}

2 changes: 1 addition & 1 deletion core/model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type User struct {
InitialRole string `json:"initial_role"`
Groups []string `json:"groups" gorm:"-"`
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
CreatedAt time.Time `json:"created_at" gorm:"index"`
}

func (User) TableName() string {
Expand Down
Loading
Loading