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
18 changes: 18 additions & 0 deletions docs/parameters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2520,6 +2520,24 @@ type: stringSlice
default: []
components: ["cache", "director", "origin", "registry"]
---
name: Server.AdminGroups
description: |+
A string slice of group names that grant admin permission for the server admin website.
Users who belong to any of these groups will be granted admin privileges, regardless of their username.

This is useful when you want to grant admin access based on group membership rather than individual user identifiers.

Group information is obtained from the issuer configuration (`Issuer.GroupSource`). Depending on the group source:
- If `Issuer.GroupSource` is `file`: Groups are read from the file specified by `Issuer.GroupFile`.
- If `Issuer.GroupSource` is `oidc`: Groups are extracted from the OIDC provider token using the claim specified by `Issuer.OIDCGroupClaim` (defaults to "groups").
- If `Issuer.GroupSource` is `internal`: Groups are read from the Pelican server's internal SQLite database.

Note: This works in conjunction with Server.UIAdminUsers. A user can be granted admin access either by being
listed in Server.UIAdminUsers or by belonging to a group listed in Server.AdminGroups.
type: stringSlice
default: []
components: ["cache", "director", "origin", "registry"]
---
name: Server.StartupTimeout
description: |+
The amount of time the pelican server will wait for its components and services to startup.
Expand Down
12 changes: 6 additions & 6 deletions origin/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ func handleUpdateCollection(ctx *gin.Context) {
return
}

isAdmin, _ := web_ui.CheckAdmin(user)
isAdmin, _ := web_ui.CheckAdmin(user, groups)

var visibility database.Visibility
if req.Visibility != nil {
Expand Down Expand Up @@ -885,7 +885,7 @@ func handlePutCollectionMetadata(ctx *gin.Context) {
return
}

isAdmin, _ := web_ui.CheckAdmin(user)
isAdmin, _ := web_ui.CheckAdmin(user, groups)

err = database.UpsertCollectionMetadata(database.ServerDatabase, ctx.Param("id"), user, groups, key, value, isAdmin)
if err != nil {
Expand Down Expand Up @@ -941,7 +941,7 @@ func handleDeleteCollectionMetadata(ctx *gin.Context) {
return
}

isAdmin, _ := web_ui.CheckAdmin(user)
isAdmin, _ := web_ui.CheckAdmin(user, groups)

err = database.DeleteCollectionMetadata(database.ServerDatabase, ctx.Param("id"), user, groups, key, isAdmin)
if err != nil {
Expand Down Expand Up @@ -1056,7 +1056,7 @@ func handleDeleteCollection(ctx *gin.Context) {
}

// we will use this check to determine if we can bypass the collection owner check
isAdmin, _ := web_ui.CheckAdmin(user)
isAdmin, _ := web_ui.CheckAdmin(user, groups)

err = database.DeleteCollection(database.ServerDatabase, ctx.Param("id"), user, groups, isAdmin)
if err != nil {
Expand Down Expand Up @@ -1175,7 +1175,7 @@ func handleGrantCollectionAcl(ctx *gin.Context) {
return
}

isAdmin, _ := web_ui.CheckAdmin(user)
isAdmin, _ := web_ui.CheckAdmin(user, groups)

err = database.GrantCollectionAcl(database.ServerDatabase, ctx.Param("id"), user, groups, req.GroupID, role, req.ExpiresAt, isAdmin)
if err != nil {
Expand Down Expand Up @@ -1248,7 +1248,7 @@ func handleRevokeCollectionAcl(ctx *gin.Context) {
return
}

isAdmin, _ := web_ui.CheckAdmin(user)
isAdmin, _ := web_ui.CheckAdmin(user, groups)

err = database.RevokeCollectionAcl(database.ServerDatabase, ctx.Param("id"), user, groups, req.GroupID, role, isAdmin)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions param/parameters.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions param/parameters_struct.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 18 additions & 4 deletions registry/registry_ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,16 @@ func getNamespaceRegFields(ctx *gin.Context) {
// POST /namespaces
// PUT /namespaces/:id
func createUpdateNamespace(ctx *gin.Context, isUpdate bool) {
user := ctx.GetString("User")
accessToken := ctx.Query("access_token")
isAdmin, _ := web_ui.CheckAdmin(user)
user, _, groups, err := web_ui.GetUserGroups(ctx)
if err != nil {
log.Error("Failed to get user groups: ", err)
ctx.JSON(http.StatusInternalServerError, server_structs.SimpleApiResp{
Status: server_structs.RespFailed,
Msg: "Failed to get user groups"})
return
}
isAdmin, _ := web_ui.CheckAdmin(user, groups)

id := 0 // namespace ID when doing update, will be populated later
if user == "" {
Expand Down Expand Up @@ -654,7 +661,14 @@ func createUpdateNamespace(ctx *gin.Context, isUpdate bool) {
//
// GET /namesapces/:id
func getNamespace(ctx *gin.Context) {
user := ctx.GetString("User")
user, _, groups, err := web_ui.GetUserGroups(ctx)
if err != nil {
log.Error("Failed to get user groups: ", err)
ctx.JSON(http.StatusInternalServerError, server_structs.SimpleApiResp{
Status: server_structs.RespFailed,
Msg: "Failed to get user groups"})
return
}
idStr := ctx.Param("id")
accessToken := ctx.Query("access_token")
id, err := strconv.Atoi(idStr)
Expand All @@ -681,7 +695,7 @@ func getNamespace(ctx *gin.Context) {
return
}

isAdmin, _ := web_ui.CheckAdmin(user)
isAdmin, _ := web_ui.CheckAdmin(user, groups)
belongsTo := false

if !isAdmin { // Not admin, need to check if the namespace belongs to the user
Expand Down
55 changes: 42 additions & 13 deletions web_ui/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ func setLoginCookie(ctx *gin.Context, userRecord *database.User, groups []string
// For backwards compatibility (see #398), add additional scopes
// for expert admins who extract the login cookie from their browser
// and use it to query monitoring endpoints directly.
if isAdmin, _ := CheckAdmin(loginCookieTokenCfg.Subject); isAdmin {
if isAdmin, _ := CheckAdmin(loginCookieTokenCfg.Subject, groups); isAdmin {
loginCookieTokenCfg.AddScopes(token_scopes.Monitoring_Query, token_scopes.Monitoring_Scrape)
}

Expand Down Expand Up @@ -368,20 +368,42 @@ func RequireAuthMiddleware(ctx *gin.Context) {
// indicating the error message.
//
// Note that by default it only checks if user == "admin". If you have a custom list of admin identifiers
// to check, you should set Server.UIAdminUsers. See parameters.yaml for details.
func CheckAdmin(user string) (isAdmin bool, message string) {
// to check, you should set Server.UIAdminUsers. If you want to grant admin privileges based on group
// membership, you should set Server.AdminGroups.
func CheckAdmin(user string, groups []string) (isAdmin bool, message string) {
if user == "admin" {
return true, ""
}
adminList := param.Server_UIAdminUsers.GetStringSlice()
if !param.Server_UIAdminUsers.IsSet() {
return false, "Server.UIAdminUsers is not set, and user is not root user. Admin check returns false"

// Check admin groups if groups are provided
if len(groups) > 0 {
adminGroups := param.Server_AdminGroups.GetStringSlice()
if param.Server_AdminGroups.IsSet() && len(adminGroups) > 0 {
for _, userGroup := range groups {
for _, adminGroup := range adminGroups {
if userGroup == adminGroup {
return true, ""
}
}
}
}
}
for _, admin := range adminList {
if user == admin {
return true, ""

// Check admin users
adminList := param.Server_UIAdminUsers.GetStringSlice()
if param.Server_UIAdminUsers.IsSet() {
for _, admin := range adminList {
if user == admin {
return true, ""
}
}
}

// If neither admin groups nor admin users are configured, and user is not "admin", deny access
if !param.Server_AdminGroups.IsSet() && !param.Server_UIAdminUsers.IsSet() {
return false, "Server.UIAdminUsers and Server.UIAdminGroups are not set, and user is not root user. Admin check returns false"
}

return false, "You don't have permission to perform this action"
}

Expand All @@ -398,7 +420,14 @@ func AdminAuthHandler(ctx *gin.Context) {
})
return
}
isAdmin, msg := CheckAdmin(user)
// Get groups from context if available
var groups []string
if groupsIface, exists := ctx.Get("Groups"); exists {
if groupsSlice, ok := groupsIface.([]string); ok {
groups = groupsSlice
}
}
isAdmin, msg := CheckAdmin(user, groups)
if isAdmin {
ctx.Next()
return
Expand All @@ -419,7 +448,7 @@ func DowntimeAuthHandler(ctx *gin.Context) {
user, _, groups, err := GetUserGroups(ctx)
if user != "" && err == nil {
// User has valid cookie, check if admin
isAdmin, _ := CheckAdmin(user)
isAdmin, _ := CheckAdmin(user, groups)
if isAdmin {
ctx.Set("User", user)
ctx.Set("Groups", groups)
Expand Down Expand Up @@ -641,7 +670,7 @@ func logoutHandler(ctx *gin.Context) {
// Returns the authentication status of the current user, including user id and role
func whoamiHandler(ctx *gin.Context) {
res := WhoAmIRes{}
if user, _, _, err := GetUserGroups(ctx); err != nil || user == "" {
if user, _, groups, err := GetUserGroups(ctx); err != nil || user == "" {
res.Authenticated = false
ctx.JSON(http.StatusOK, res)
} else {
Expand All @@ -650,7 +679,7 @@ func whoamiHandler(ctx *gin.Context) {

// Set header to carry CSRF token
ctx.Header("X-CSRF-Token", csrf.Token(ctx.Request))
isAdmin, _ := CheckAdmin(user)
isAdmin, _ := CheckAdmin(user, groups)
if isAdmin {
res.Role = AdminRole
} else {
Expand Down
Loading
Loading