mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-24 01:11:52 +03:00
Compare commits
4 Commits
main
...
feat/user-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b02dd34c36 | ||
|
|
c757cfa8c8 | ||
|
|
ca40251776 | ||
|
|
f61c784988 |
@@ -72,7 +72,7 @@ type UserController struct {
|
|||||||
// @Description Retrieve all groups a specific user belongs to
|
// @Description Retrieve all groups a specific user belongs to
|
||||||
// @Tags Users,User Groups
|
// @Tags Users,User Groups
|
||||||
// @Param id path string true "User ID"
|
// @Param id path string true "User ID"
|
||||||
// @Success 200 {array} dto.UserGroupDtoWithUsers
|
// @Success 200 {array} dto.UserGroupDto
|
||||||
// @Router /api/users/{id}/groups [get]
|
// @Router /api/users/{id}/groups [get]
|
||||||
func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
|
func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
|
||||||
userID := c.Param("id")
|
userID := c.Param("id")
|
||||||
@@ -82,7 +82,7 @@ func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupsDto []dto.UserGroupDtoWithUsers
|
var groupsDto []dto.UserGroupDto
|
||||||
if err := dto.MapStructList(groups, &groupsDto); err != nil {
|
if err := dto.MapStructList(groups, &groupsDto); err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func NewUserGroupController(group *gin.RouterGroup, authMiddleware *middleware.A
|
|||||||
userGroupsGroup.PUT("/:id", ugc.update)
|
userGroupsGroup.PUT("/:id", ugc.update)
|
||||||
userGroupsGroup.DELETE("/:id", ugc.delete)
|
userGroupsGroup.DELETE("/:id", ugc.delete)
|
||||||
userGroupsGroup.PUT("/:id/users", ugc.updateUsers)
|
userGroupsGroup.PUT("/:id/users", ugc.updateUsers)
|
||||||
|
userGroupsGroup.PUT("/:id/allowed-oidc-clients", ugc.updateAllowedOidcClients)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ type UserGroupController struct {
|
|||||||
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
||||||
// @Param sort[column] query string false "Column to sort by"
|
// @Param sort[column] query string false "Column to sort by"
|
||||||
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
||||||
// @Success 200 {object} dto.Paginated[dto.UserGroupDtoWithUserCount]
|
// @Success 200 {object} dto.Paginated[dto.UserGroupMinimalDto]
|
||||||
// @Router /api/user-groups [get]
|
// @Router /api/user-groups [get]
|
||||||
func (ugc *UserGroupController) list(c *gin.Context) {
|
func (ugc *UserGroupController) list(c *gin.Context) {
|
||||||
searchTerm := c.Query("search")
|
searchTerm := c.Query("search")
|
||||||
@@ -57,9 +58,9 @@ func (ugc *UserGroupController) list(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map the user groups to DTOs
|
// Map the user groups to DTOs
|
||||||
var groupsDto = make([]dto.UserGroupDtoWithUserCount, len(groups))
|
var groupsDto = make([]dto.UserGroupMinimalDto, len(groups))
|
||||||
for i, group := range groups {
|
for i, group := range groups {
|
||||||
var groupDto dto.UserGroupDtoWithUserCount
|
var groupDto dto.UserGroupMinimalDto
|
||||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -72,7 +73,7 @@ func (ugc *UserGroupController) list(c *gin.Context) {
|
|||||||
groupsDto[i] = groupDto
|
groupsDto[i] = groupDto
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, dto.Paginated[dto.UserGroupDtoWithUserCount]{
|
c.JSON(http.StatusOK, dto.Paginated[dto.UserGroupMinimalDto]{
|
||||||
Data: groupsDto,
|
Data: groupsDto,
|
||||||
Pagination: pagination,
|
Pagination: pagination,
|
||||||
})
|
})
|
||||||
@@ -85,7 +86,7 @@ func (ugc *UserGroupController) list(c *gin.Context) {
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "User Group ID"
|
// @Param id path string true "User Group ID"
|
||||||
// @Success 200 {object} dto.UserGroupDtoWithUsers
|
// @Success 200 {object} dto.UserGroupDto
|
||||||
// @Router /api/user-groups/{id} [get]
|
// @Router /api/user-groups/{id} [get]
|
||||||
func (ugc *UserGroupController) get(c *gin.Context) {
|
func (ugc *UserGroupController) get(c *gin.Context) {
|
||||||
group, err := ugc.UserGroupService.Get(c.Request.Context(), c.Param("id"))
|
group, err := ugc.UserGroupService.Get(c.Request.Context(), c.Param("id"))
|
||||||
@@ -94,7 +95,7 @@ func (ugc *UserGroupController) get(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupDto dto.UserGroupDtoWithUsers
|
var groupDto dto.UserGroupDto
|
||||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -110,7 +111,7 @@ func (ugc *UserGroupController) get(c *gin.Context) {
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param userGroup body dto.UserGroupCreateDto true "User group information"
|
// @Param userGroup body dto.UserGroupCreateDto true "User group information"
|
||||||
// @Success 201 {object} dto.UserGroupDtoWithUsers "Created user group"
|
// @Success 201 {object} dto.UserGroupDto "Created user group"
|
||||||
// @Router /api/user-groups [post]
|
// @Router /api/user-groups [post]
|
||||||
func (ugc *UserGroupController) create(c *gin.Context) {
|
func (ugc *UserGroupController) create(c *gin.Context) {
|
||||||
var input dto.UserGroupCreateDto
|
var input dto.UserGroupCreateDto
|
||||||
@@ -125,7 +126,7 @@ func (ugc *UserGroupController) create(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupDto dto.UserGroupDtoWithUsers
|
var groupDto dto.UserGroupDto
|
||||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -142,7 +143,7 @@ func (ugc *UserGroupController) create(c *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "User Group ID"
|
// @Param id path string true "User Group ID"
|
||||||
// @Param userGroup body dto.UserGroupCreateDto true "User group information"
|
// @Param userGroup body dto.UserGroupCreateDto true "User group information"
|
||||||
// @Success 200 {object} dto.UserGroupDtoWithUsers "Updated user group"
|
// @Success 200 {object} dto.UserGroupDto "Updated user group"
|
||||||
// @Router /api/user-groups/{id} [put]
|
// @Router /api/user-groups/{id} [put]
|
||||||
func (ugc *UserGroupController) update(c *gin.Context) {
|
func (ugc *UserGroupController) update(c *gin.Context) {
|
||||||
var input dto.UserGroupCreateDto
|
var input dto.UserGroupCreateDto
|
||||||
@@ -157,7 +158,7 @@ func (ugc *UserGroupController) update(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupDto dto.UserGroupDtoWithUsers
|
var groupDto dto.UserGroupDto
|
||||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -192,7 +193,7 @@ func (ugc *UserGroupController) delete(c *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "User Group ID"
|
// @Param id path string true "User Group ID"
|
||||||
// @Param users body dto.UserGroupUpdateUsersDto true "List of user IDs to assign to this group"
|
// @Param users body dto.UserGroupUpdateUsersDto true "List of user IDs to assign to this group"
|
||||||
// @Success 200 {object} dto.UserGroupDtoWithUsers
|
// @Success 200 {object} dto.UserGroupDto
|
||||||
// @Router /api/user-groups/{id}/users [put]
|
// @Router /api/user-groups/{id}/users [put]
|
||||||
func (ugc *UserGroupController) updateUsers(c *gin.Context) {
|
func (ugc *UserGroupController) updateUsers(c *gin.Context) {
|
||||||
var input dto.UserGroupUpdateUsersDto
|
var input dto.UserGroupUpdateUsersDto
|
||||||
@@ -207,7 +208,7 @@ func (ugc *UserGroupController) updateUsers(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupDto dto.UserGroupDtoWithUsers
|
var groupDto dto.UserGroupDto
|
||||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -215,3 +216,35 @@ func (ugc *UserGroupController) updateUsers(c *gin.Context) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, groupDto)
|
c.JSON(http.StatusOK, groupDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateAllowedOidcClients godoc
|
||||||
|
// @Summary Update allowed OIDC clients
|
||||||
|
// @Description Update the OIDC clients allowed for a specific user group
|
||||||
|
// @Tags OIDC
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "User Group ID"
|
||||||
|
// @Param groups body dto.UserGroupUpdateAllowedOidcClientsDto true "OIDC client IDs to allow"
|
||||||
|
// @Success 200 {object} dto.UserGroupDto "Updated user group"
|
||||||
|
// @Router /api/user-groups/{id}/allowed-oidc-clients [put]
|
||||||
|
func (ugc *UserGroupController) updateAllowedOidcClients(c *gin.Context) {
|
||||||
|
var input dto.UserGroupUpdateAllowedOidcClientsDto
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
_ = c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userGroup, err := ugc.UserGroupService.UpdateAllowedOidcClient(c.Request.Context(), c.Param("id"), input)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var userGroupDto dto.UserGroupDto
|
||||||
|
if err := dto.MapStruct(userGroup, &userGroupDto); err != nil {
|
||||||
|
_ = c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, userGroupDto)
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ type OidcClientDto struct {
|
|||||||
IsPublic bool `json:"isPublic"`
|
IsPublic bool `json:"isPublic"`
|
||||||
PkceEnabled bool `json:"pkceEnabled"`
|
PkceEnabled bool `json:"pkceEnabled"`
|
||||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
Credentials OidcClientCredentialsDto `json:"credentials"`
|
||||||
|
IsGroupRestricted bool `json:"isGroupRestricted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcClientWithAllowedUserGroupsDto struct {
|
type OidcClientWithAllowedUserGroupsDto struct {
|
||||||
OidcClientDto
|
OidcClientDto
|
||||||
AllowedUserGroups []UserGroupDtoWithUserCount `json:"allowedUserGroups"`
|
AllowedUserGroups []UserGroupMinimalDto `json:"allowedUserGroups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcClientWithAllowedGroupsCountDto struct {
|
type OidcClientWithAllowedGroupsCountDto struct {
|
||||||
@@ -43,6 +44,7 @@ type OidcClientUpdateDto struct {
|
|||||||
HasDarkLogo bool `json:"hasDarkLogo"`
|
HasDarkLogo bool `json:"hasDarkLogo"`
|
||||||
LogoURL *string `json:"logoUrl"`
|
LogoURL *string `json:"logoUrl"`
|
||||||
DarkLogoURL *string `json:"darkLogoUrl"`
|
DarkLogoURL *string `json:"darkLogoUrl"`
|
||||||
|
IsGroupRestricted bool `json:"isGroupRestricted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcClientCreateDto struct {
|
type OidcClientCreateDto struct {
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ type SignupTokenCreateDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SignupTokenDto struct {
|
type SignupTokenDto struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
ExpiresAt datatype.DateTime `json:"expiresAt"`
|
ExpiresAt datatype.DateTime `json:"expiresAt"`
|
||||||
UsageLimit int `json:"usageLimit"`
|
UsageLimit int `json:"usageLimit"`
|
||||||
UsageCount int `json:"usageCount"`
|
UsageCount int `json:"usageCount"`
|
||||||
UserGroups []UserGroupDto `json:"userGroups"`
|
UserGroups []UserGroupMinimalDto `json:"userGroups"`
|
||||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UserDto struct {
|
type UserDto struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email *string `json:"email" `
|
Email *string `json:"email" `
|
||||||
FirstName string `json:"firstName"`
|
FirstName string `json:"firstName"`
|
||||||
LastName *string `json:"lastName"`
|
LastName *string `json:"lastName"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
Locale *string `json:"locale"`
|
Locale *string `json:"locale"`
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
UserGroups []UserGroupDto `json:"userGroups"`
|
UserGroups []UserGroupMinimalDto `json:"userGroups"`
|
||||||
LdapID *string `json:"ldapId"`
|
LdapID *string `json:"ldapId"`
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserCreateDto struct {
|
type UserCreateDto struct {
|
||||||
|
|||||||
@@ -8,25 +8,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UserGroupDto struct {
|
type UserGroupDto struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FriendlyName string `json:"friendlyName"`
|
FriendlyName string `json:"friendlyName"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
LdapID *string `json:"ldapId"`
|
LdapID *string `json:"ldapId"`
|
||||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
|
Users []UserDto `json:"users"`
|
||||||
|
AllowedOidcClients []OidcClientMetaDataDto `json:"allowedOidcClients"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserGroupDtoWithUsers struct {
|
type UserGroupMinimalDto struct {
|
||||||
ID string `json:"id"`
|
|
||||||
FriendlyName string `json:"friendlyName"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
|
||||||
Users []UserDto `json:"users"`
|
|
||||||
LdapID *string `json:"ldapId"`
|
|
||||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserGroupDtoWithUserCount struct {
|
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FriendlyName string `json:"friendlyName"`
|
FriendlyName string `json:"friendlyName"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -36,6 +28,10 @@ type UserGroupDtoWithUserCount struct {
|
|||||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserGroupUpdateAllowedOidcClientsDto struct {
|
||||||
|
OidcClientIDs []string `json:"oidcClientIds" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type UserGroupCreateDto struct {
|
type UserGroupCreateDto struct {
|
||||||
FriendlyName string `json:"friendlyName" binding:"required,min=2,max=50" unorm:"nfc"`
|
FriendlyName string `json:"friendlyName" binding:"required,min=2,max=50" unorm:"nfc"`
|
||||||
Name string `json:"name" binding:"required,min=2,max=255" unorm:"nfc"`
|
Name string `json:"name" binding:"required,min=2,max=255" unorm:"nfc"`
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ type OidcClient struct {
|
|||||||
RequiresReauthentication bool `sortable:"true" filterable:"true"`
|
RequiresReauthentication bool `sortable:"true" filterable:"true"`
|
||||||
Credentials OidcClientCredentials
|
Credentials OidcClientCredentials
|
||||||
LaunchURL *string
|
LaunchURL *string
|
||||||
|
IsGroupRestricted bool
|
||||||
|
|
||||||
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
|
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
|
||||||
CreatedByID *string
|
CreatedByID *string
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package model
|
|||||||
|
|
||||||
type UserGroup struct {
|
type UserGroup struct {
|
||||||
Base
|
Base
|
||||||
FriendlyName string `sortable:"true"`
|
FriendlyName string `sortable:"true"`
|
||||||
Name string `sortable:"true"`
|
Name string `sortable:"true"`
|
||||||
LdapID *string
|
LdapID *string
|
||||||
Users []User `gorm:"many2many:user_groups_users;"`
|
Users []User `gorm:"many2many:user_groups_users;"`
|
||||||
CustomClaims []CustomClaim
|
CustomClaims []CustomClaim
|
||||||
|
AllowedOidcClients []OidcClient `gorm:"many2many:oidc_clients_allowed_user_groups;"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,10 +169,11 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "606c7782-f2b1-49e5-8ea9-26eb1b06d018",
|
ID: "606c7782-f2b1-49e5-8ea9-26eb1b06d018",
|
||||||
},
|
},
|
||||||
Name: "Immich",
|
Name: "Immich",
|
||||||
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
||||||
CallbackURLs: model.UrlList{"http://immich/auth/callback"},
|
CallbackURLs: model.UrlList{"http://immich/auth/callback"},
|
||||||
CreatedByID: utils.Ptr(users[1].ID),
|
CreatedByID: utils.Ptr(users[1].ID),
|
||||||
|
IsGroupRestricted: true,
|
||||||
AllowedUserGroups: []model.UserGroup{
|
AllowedUserGroups: []model.UserGroup{
|
||||||
userGroups[1],
|
userGroups[1],
|
||||||
},
|
},
|
||||||
@@ -185,6 +186,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
Secret: "$2a$10$xcRReBsvkI1XI6FG8xu/pOgzeF00bH5Wy4d/NThwcdi3ZBpVq/B9a", // n4VfQeXlTzA6yKpWbR9uJcMdSx2qH0Lo
|
Secret: "$2a$10$xcRReBsvkI1XI6FG8xu/pOgzeF00bH5Wy4d/NThwcdi3ZBpVq/B9a", // n4VfQeXlTzA6yKpWbR9uJcMdSx2qH0Lo
|
||||||
CallbackURLs: model.UrlList{"http://tailscale/auth/callback"},
|
CallbackURLs: model.UrlList{"http://tailscale/auth/callback"},
|
||||||
LogoutCallbackURLs: model.UrlList{"http://tailscale/auth/logout/callback"},
|
LogoutCallbackURLs: model.UrlList{"http://tailscale/auth/logout/callback"},
|
||||||
|
IsGroupRestricted: true,
|
||||||
CreatedByID: utils.Ptr(users[0].ID),
|
CreatedByID: utils.Ptr(users[0].ID),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ func (s *OidcService) hasAuthorizedClientInternal(ctx context.Context, clientID,
|
|||||||
|
|
||||||
// IsUserGroupAllowedToAuthorize checks if the user group of the user is allowed to authorize the client
|
// IsUserGroupAllowedToAuthorize checks if the user group of the user is allowed to authorize the client
|
||||||
func (s *OidcService) IsUserGroupAllowedToAuthorize(user model.User, client model.OidcClient) bool {
|
func (s *OidcService) IsUserGroupAllowedToAuthorize(user model.User, client model.OidcClient) bool {
|
||||||
if len(client.AllowedUserGroups) == 0 {
|
if !client.IsGroupRestricted {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,6 +778,14 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
|
|||||||
|
|
||||||
updateOIDCClientModelFromDto(&client, &input)
|
updateOIDCClientModelFromDto(&client, &input)
|
||||||
|
|
||||||
|
if !input.IsGroupRestricted {
|
||||||
|
// Clear allowed user groups if the restriction is removed
|
||||||
|
err = tx.Model(&client).Association("AllowedUserGroups").Clear()
|
||||||
|
if err != nil {
|
||||||
|
return model.OidcClient{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = tx.WithContext(ctx).Save(&client).Error
|
err = tx.WithContext(ctx).Save(&client).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.OidcClient{}, err
|
return model.OidcClient{}, err
|
||||||
@@ -816,6 +824,7 @@ func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClien
|
|||||||
client.PkceEnabled = input.IsPublic || input.PkceEnabled
|
client.PkceEnabled = input.IsPublic || input.PkceEnabled
|
||||||
client.RequiresReauthentication = input.RequiresReauthentication
|
client.RequiresReauthentication = input.RequiresReauthentication
|
||||||
client.LaunchURL = input.LaunchURL
|
client.LaunchURL = input.LaunchURL
|
||||||
|
client.IsGroupRestricted = input.IsGroupRestricted
|
||||||
|
|
||||||
// Credentials
|
// Credentials
|
||||||
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
|
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ func (s *UserGroupService) getInternal(ctx context.Context, id string, tx *gorm.
|
|||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Preload("CustomClaims").
|
Preload("CustomClaims").
|
||||||
Preload("Users").
|
Preload("Users").
|
||||||
|
Preload("AllowedOidcClients").
|
||||||
First(&group).
|
First(&group).
|
||||||
Error
|
Error
|
||||||
return group, err
|
return group, err
|
||||||
@@ -248,3 +249,54 @@ func (s *UserGroupService) GetUserCountOfGroup(ctx context.Context, id string) (
|
|||||||
Count()
|
Count()
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UserGroupService) UpdateAllowedOidcClient(ctx context.Context, id string, input dto.UserGroupUpdateAllowedOidcClientsDto) (group model.UserGroup, err error) {
|
||||||
|
tx := s.db.Begin()
|
||||||
|
defer func() {
|
||||||
|
tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
group, err = s.getInternal(ctx, id, tx)
|
||||||
|
if err != nil {
|
||||||
|
return model.UserGroup{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the clients based on the client IDs
|
||||||
|
var clients []model.OidcClient
|
||||||
|
if len(input.OidcClientIDs) > 0 {
|
||||||
|
err = tx.
|
||||||
|
WithContext(ctx).
|
||||||
|
Where("id IN (?)", input.OidcClientIDs).
|
||||||
|
Find(&clients).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return model.UserGroup{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the current clients with the new set of clients
|
||||||
|
err = tx.
|
||||||
|
WithContext(ctx).
|
||||||
|
Model(&group).
|
||||||
|
Association("AllowedOidcClients").
|
||||||
|
Replace(clients)
|
||||||
|
if err != nil {
|
||||||
|
return model.UserGroup{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated group
|
||||||
|
err = tx.
|
||||||
|
WithContext(ctx).
|
||||||
|
Save(&group).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return model.UserGroup{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit().Error
|
||||||
|
if err != nil {
|
||||||
|
return model.UserGroup{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return group, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE oidc_clients DROP COLUMN is_group_restricted;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
ALTER TABLE oidc_clients
|
||||||
|
ADD COLUMN is_group_restricted boolean NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
UPDATE oidc_clients oc
|
||||||
|
SET is_group_restricted =
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM oidc_clients_allowed_user_groups a
|
||||||
|
WHERE a.oidc_client_id = oc.id
|
||||||
|
);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE oidc_clients DROP COLUMN is_group_restricted;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
PRAGMA foreign_keys= OFF;
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE oidc_clients
|
||||||
|
ADD COLUMN is_group_restricted BOOLEAN NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
UPDATE oidc_clients
|
||||||
|
SET is_group_restricted = (SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END
|
||||||
|
FROM oidc_clients_allowed_user_groups
|
||||||
|
WHERE oidc_clients_allowed_user_groups.oidc_client_id = oidc_clients.id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
PRAGMA foreign_keys= ON;
|
||||||
@@ -301,13 +301,17 @@
|
|||||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.",
|
"are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.",
|
||||||
"generate": "Generate",
|
"generate": "Generate",
|
||||||
"new_client_secret_created_successfully": "New client secret created successfully",
|
"new_client_secret_created_successfully": "New client secret created successfully",
|
||||||
"allowed_user_groups_updated_successfully": "Allowed user groups updated successfully",
|
|
||||||
"oidc_client_name": "OIDC Client {name}",
|
"oidc_client_name": "OIDC Client {name}",
|
||||||
"client_id": "Client ID",
|
"client_id": "Client ID",
|
||||||
"client_secret": "Client secret",
|
"client_secret": "Client secret",
|
||||||
"show_more_details": "Show more details",
|
"show_more_details": "Show more details",
|
||||||
"allowed_user_groups": "Allowed User Groups",
|
"allowed_user_groups": "Allowed User Groups",
|
||||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.",
|
"allowed_user_groups_description": "Select the user groups whose members are allowed to sign in to this client.",
|
||||||
|
"allowed_user_groups_status_unrestricted_description": "No user group restrictions are applied. Any user can sign in to this client.",
|
||||||
|
"unrestrict": "Unrestrict",
|
||||||
|
"restrict": "Restrict",
|
||||||
|
"user_groups_restriction_updated_successfully": "User groups restriction updated successfully",
|
||||||
|
"allowed_user_groups_updated_successfully": "Allowed user groups updated successfully",
|
||||||
"favicon": "Favicon",
|
"favicon": "Favicon",
|
||||||
"light_mode_logo": "Light Mode Logo",
|
"light_mode_logo": "Light Mode Logo",
|
||||||
"dark_mode_logo": "Dark Mode Logo",
|
"dark_mode_logo": "Dark Mode Logo",
|
||||||
@@ -471,5 +475,10 @@
|
|||||||
"light": "Light",
|
"light": "Light",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"signup_token_user_groups_description": "Automatically assign these groups to users who sign up using this token."
|
"signup_token_user_groups_description": "Automatically assign these groups to users who sign up using this token.",
|
||||||
|
"allowed_oidc_clients": "Allowed OIDC Clients",
|
||||||
|
"allowed_oidc_clients_description": "Select the OIDC clients that members of this user group are allowed to sign in to.",
|
||||||
|
"unrestrict_oidc_client": "Unrestrict {clientName}",
|
||||||
|
"confirm_unrestrict_oidc_client_description": "Are you sure you want to unrestrict the OIDC client <b>{clientName}</b>? This will remove all group assignments for this client and any user will be able to sign in.",
|
||||||
|
"allowed_oidc_clients_updated_successfully": "Allowed OIDC clients updated successfully"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
defaultExpanded = false,
|
defaultExpanded = false,
|
||||||
|
forcedExpanded,
|
||||||
|
button,
|
||||||
icon,
|
icon,
|
||||||
children
|
children
|
||||||
}: {
|
}: {
|
||||||
@@ -19,7 +21,9 @@
|
|||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
defaultExpanded?: boolean;
|
defaultExpanded?: boolean;
|
||||||
|
forcedExpanded?: boolean;
|
||||||
icon?: typeof IconType;
|
icon?: typeof IconType;
|
||||||
|
button?: Snippet;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -47,6 +51,12 @@
|
|||||||
}
|
}
|
||||||
loadExpandedState();
|
loadExpandedState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (forcedExpanded !== undefined) {
|
||||||
|
expanded = forcedExpanded;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
@@ -63,11 +73,18 @@
|
|||||||
<Card.Description>{description}</Card.Description>
|
<Card.Description>{description}</Card.Description>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Button class="ml-10 h-8 p-3" variant="ghost" aria-label={m.expand_card()}>
|
{#if button}
|
||||||
<LucideChevronDown
|
{@render button()}
|
||||||
class={cn('size-5 transition-transform duration-200', expanded && 'rotate-180 transform')}
|
{:else}
|
||||||
/>
|
<Button class="ml-10 h-8 p-3" variant="ghost" aria-label={m.expand_card()}>
|
||||||
</Button>
|
<LucideChevronDown
|
||||||
|
class={cn(
|
||||||
|
'size-5 transition-transform duration-200',
|
||||||
|
expanded && 'rotate-180 transform'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
selectedIds = $bindable(),
|
selectedIds = $bindable(),
|
||||||
withoutSearch = false,
|
withoutSearch = false,
|
||||||
selectionDisabled = false,
|
selectionDisabled = false,
|
||||||
|
rowSelectionDisabled,
|
||||||
fetchCallback,
|
fetchCallback,
|
||||||
defaultSort,
|
defaultSort,
|
||||||
columns,
|
columns,
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
selectedIds?: string[];
|
selectedIds?: string[];
|
||||||
withoutSearch?: boolean;
|
withoutSearch?: boolean;
|
||||||
selectionDisabled?: boolean;
|
selectionDisabled?: boolean;
|
||||||
|
rowSelectionDisabled?: (item: T) => boolean;
|
||||||
fetchCallback: (requestOptions: ListRequestOptions) => Promise<Paginated<T>>;
|
fetchCallback: (requestOptions: ListRequestOptions) => Promise<Paginated<T>>;
|
||||||
defaultSort?: SortRequest;
|
defaultSort?: SortRequest;
|
||||||
columns: AdvancedTableColumn<T>[];
|
columns: AdvancedTableColumn<T>[];
|
||||||
@@ -91,7 +93,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function onAllCheck(checked: boolean) {
|
async function onAllCheck(checked: boolean) {
|
||||||
const pageIds = items!.data.map((item) => item.id);
|
const pageIds = items!.data
|
||||||
|
.filter((item) => !rowSelectionDisabled?.(item))
|
||||||
|
.map((item) => item.id);
|
||||||
const current = selectedIds ?? [];
|
const current = selectedIds ?? [];
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
@@ -264,7 +268,7 @@
|
|||||||
{#if selectedIds}
|
{#if selectedIds}
|
||||||
<Table.Cell class="w-12">
|
<Table.Cell class="w-12">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={selectionDisabled}
|
disabled={selectionDisabled || rowSelectionDisabled?.(item)}
|
||||||
checked={selectedIds.includes(item.id)}
|
checked={selectedIds.includes(item.id)}
|
||||||
onCheckedChange={(c: boolean) => onCheck(c, item.id)}
|
onCheckedChange={(c: boolean) => onCheck(c, item.id)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import UserGroupService from '$lib/services/user-group-service';
|
import UserGroupService from '$lib/services/user-group-service';
|
||||||
import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
|
import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
|
||||||
import type { UserGroupWithUserCount } from '$lib/types/user-group.type';
|
import type { UserGroupMinimal } from '$lib/types/user-group.type';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
selectionDisabled = false,
|
selectionDisabled = false,
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
const userGroupService = new UserGroupService();
|
const userGroupService = new UserGroupService();
|
||||||
|
|
||||||
const columns: AdvancedTableColumn<UserGroupWithUserCount>[] = [
|
const columns: AdvancedTableColumn<UserGroupMinimal>[] = [
|
||||||
{ label: 'ID', column: 'id', hidden: true },
|
{ label: 'ID', column: 'id', hidden: true },
|
||||||
{ label: m.friendly_name(), column: 'friendlyName', sortable: true },
|
{ label: m.friendly_name(), column: 'friendlyName', sortable: true },
|
||||||
{ label: m.name(), column: 'name', sortable: true },
|
{ label: m.name(), column: 'name', sortable: true },
|
||||||
|
|||||||
@@ -1,30 +1,26 @@
|
|||||||
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
|
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
|
||||||
import type {
|
import type { UserGroup, UserGroupCreate, UserGroupMinimal } from '$lib/types/user-group.type';
|
||||||
UserGroupCreate,
|
|
||||||
UserGroupWithUserCount,
|
|
||||||
UserGroupWithUsers
|
|
||||||
} from '$lib/types/user-group.type';
|
|
||||||
import APIService from './api-service';
|
import APIService from './api-service';
|
||||||
|
|
||||||
export default class UserGroupService extends APIService {
|
export default class UserGroupService extends APIService {
|
||||||
list = async (options?: ListRequestOptions) => {
|
list = async (options?: ListRequestOptions) => {
|
||||||
const res = await this.api.get('/user-groups', { params: options });
|
const res = await this.api.get('/user-groups', { params: options });
|
||||||
return res.data as Paginated<UserGroupWithUserCount>;
|
return res.data as Paginated<UserGroupMinimal>;
|
||||||
};
|
};
|
||||||
|
|
||||||
get = async (id: string) => {
|
get = async (id: string) => {
|
||||||
const res = await this.api.get(`/user-groups/${id}`);
|
const res = await this.api.get(`/user-groups/${id}`);
|
||||||
return res.data as UserGroupWithUsers;
|
return res.data as UserGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
create = async (user: UserGroupCreate) => {
|
create = async (user: UserGroupCreate) => {
|
||||||
const res = await this.api.post('/user-groups', user);
|
const res = await this.api.post('/user-groups', user);
|
||||||
return res.data as UserGroupWithUsers;
|
return res.data as UserGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
update = async (id: string, user: UserGroupCreate) => {
|
update = async (id: string, user: UserGroupCreate) => {
|
||||||
const res = await this.api.put(`/user-groups/${id}`, user);
|
const res = await this.api.put(`/user-groups/${id}`, user);
|
||||||
return res.data as UserGroupWithUsers;
|
return res.data as UserGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
remove = async (id: string) => {
|
remove = async (id: string) => {
|
||||||
@@ -33,6 +29,11 @@ export default class UserGroupService extends APIService {
|
|||||||
|
|
||||||
updateUsers = async (id: string, userIds: string[]) => {
|
updateUsers = async (id: string, userIds: string[]) => {
|
||||||
const res = await this.api.put(`/user-groups/${id}/users`, { userIds });
|
const res = await this.api.put(`/user-groups/${id}/users`, { userIds });
|
||||||
return res.data as UserGroupWithUsers;
|
return res.data as UserGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateAllowedOidcClients = async (id: string, oidcClientIds: string[]) => {
|
||||||
|
const res = await this.api.put(`/user-groups/${id}/allowed-oidc-clients`, { oidcClientIds });
|
||||||
|
return res.data as UserGroup;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export type OidcClient = OidcClientMetaData & {
|
|||||||
requiresReauthentication: boolean;
|
requiresReauthentication: boolean;
|
||||||
credentials?: OidcClientCredentials;
|
credentials?: OidcClientCredentials;
|
||||||
launchURL?: string;
|
launchURL?: string;
|
||||||
|
isGroupRestricted: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OidcClientWithAllowedUserGroups = OidcClient & {
|
export type OidcClientWithAllowedUserGroups = OidcClient & {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CustomClaim } from './custom-claim.type';
|
import type { CustomClaim } from './custom-claim.type';
|
||||||
|
import type { OidcClientMetaData } from './oidc.type';
|
||||||
import type { User } from './user.type';
|
import type { User } from './user.type';
|
||||||
|
|
||||||
export type UserGroup = {
|
export type UserGroup = {
|
||||||
@@ -8,13 +9,11 @@ export type UserGroup = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
customClaims: CustomClaim[];
|
customClaims: CustomClaim[];
|
||||||
ldapId?: string;
|
ldapId?: string;
|
||||||
};
|
|
||||||
|
|
||||||
export type UserGroupWithUsers = UserGroup & {
|
|
||||||
users: User[];
|
users: User[];
|
||||||
|
allowedOidcClients: OidcClientMetaData[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserGroupWithUserCount = UserGroup & {
|
export type UserGroupMinimal = Omit<UserGroup, 'users' | 'allowedOidcClients'> & {
|
||||||
userCount: number;
|
userCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,35 @@
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function enableGroupRestriction() {
|
||||||
|
client.isGroupRestricted = true;
|
||||||
|
await oidcService
|
||||||
|
.updateClient(client.id, client)
|
||||||
|
.then(() => toast.success(m.user_groups_restriction_updated_successfully()))
|
||||||
|
.catch(axiosErrorToast);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableGroupRestriction() {
|
||||||
|
openConfirmDialog({
|
||||||
|
title: m.unrestrict_oidc_client({ clientName: client.name }),
|
||||||
|
message: m.confirm_unrestrict_oidc_client_description({ clientName: client.name }),
|
||||||
|
confirm: {
|
||||||
|
label: m.unrestrict(),
|
||||||
|
destructive: true,
|
||||||
|
action: async () => {
|
||||||
|
client.isGroupRestricted = false;
|
||||||
|
await oidcService
|
||||||
|
.updateClient(client.id, client)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(m.user_groups_restriction_updated_successfully());
|
||||||
|
client.allowedUserGroupIds = [];
|
||||||
|
})
|
||||||
|
.catch(axiosErrorToast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function createClientSecret() {
|
async function createClientSecret() {
|
||||||
openConfirmDialog({
|
openConfirmDialog({
|
||||||
title: m.create_new_client_secret(),
|
title: m.create_new_client_secret(),
|
||||||
@@ -120,6 +149,13 @@
|
|||||||
<title>{m.oidc_client_name({ name: client.name })}</title>
|
<title>{m.oidc_client_name({ name: client.name })}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
{#snippet UnrestrictButton()}
|
||||||
|
<Button
|
||||||
|
onclick={enableGroupRestriction}
|
||||||
|
variant={client.isGroupRestricted ? 'secondary' : 'default'}>{m.restrict()}</Button
|
||||||
|
>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="text-muted-foreground flex text-sm" onclick={backNavigation.go}
|
<button type="button" class="text-muted-foreground flex text-sm" onclick={backNavigation.go}
|
||||||
><LucideChevronLeft class="size-5" /> {m.back()}</button
|
><LucideChevronLeft class="size-5" /> {m.back()}</button
|
||||||
@@ -193,10 +229,19 @@
|
|||||||
<CollapsibleCard
|
<CollapsibleCard
|
||||||
id="allowed-user-groups"
|
id="allowed-user-groups"
|
||||||
title={m.allowed_user_groups()}
|
title={m.allowed_user_groups()}
|
||||||
description={m.add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups()}
|
button={!client.isGroupRestricted ? UnrestrictButton : undefined}
|
||||||
|
forcedExpanded={client.isGroupRestricted ? undefined : false}
|
||||||
|
description={client.isGroupRestricted
|
||||||
|
? m.allowed_user_groups_description()
|
||||||
|
: m.allowed_user_groups_status_unrestricted_description()}
|
||||||
>
|
>
|
||||||
<UserGroupSelection bind:selectedGroupIds={client.allowedUserGroupIds} />
|
<UserGroupSelection
|
||||||
<div class="mt-5 flex justify-end">
|
bind:selectedGroupIds={client.allowedUserGroupIds}
|
||||||
|
selectionDisabled={!client.isGroupRestricted}
|
||||||
|
/>
|
||||||
|
<div class="mt-5 flex justify-end gap-3">
|
||||||
|
<Button onclick={disableGroupRestriction} variant="secondary">{m.unrestrict()}</Button>
|
||||||
|
|
||||||
<Button onclick={() => updateUserGroupClients(client.allowedUserGroupIds)}>{m.save()}</Button>
|
<Button onclick={() => updateUserGroupClients(client.allowedUserGroupIds)}>{m.save()}</Button>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleCard>
|
</CollapsibleCard>
|
||||||
|
|||||||
@@ -102,7 +102,8 @@
|
|||||||
logo: $inputs.logoUrl?.value ? undefined : logo,
|
logo: $inputs.logoUrl?.value ? undefined : logo,
|
||||||
logoUrl: $inputs.logoUrl?.value,
|
logoUrl: $inputs.logoUrl?.value,
|
||||||
darkLogo: $inputs.darkLogoUrl?.value ? undefined : darkLogo,
|
darkLogo: $inputs.darkLogoUrl?.value ? undefined : darkLogo,
|
||||||
darkLogoUrl: $inputs.darkLogoUrl?.value
|
darkLogoUrl: $inputs.darkLogoUrl?.value,
|
||||||
|
isGroupRestricted: existingClient?.isGroupRestricted ?? true
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasLogo = logo != null || !!$inputs.logoUrl?.value;
|
const hasLogo = logo != null || !!$inputs.logoUrl?.value;
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
import { backNavigate } from '../../users/navigate-back-util';
|
import { backNavigate } from '../../users/navigate-back-util';
|
||||||
import UserGroupForm from '../user-group-form.svelte';
|
import UserGroupForm from '../user-group-form.svelte';
|
||||||
import UserSelection from '../user-selection.svelte';
|
import UserSelection from '../user-selection.svelte';
|
||||||
|
import OidcClientSelection from './oidc-client-selection.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let userGroup = $state({
|
let userGroup = $state({
|
||||||
...data.userGroup,
|
...data.userGroup,
|
||||||
userIds: data.userGroup.users.map((u) => u.id)
|
userIds: data.userGroup.users.map((u) => u.id),
|
||||||
|
allowedOidcClientIds: data.userGroup.allowedOidcClients.map((c) => c.id)
|
||||||
});
|
});
|
||||||
|
|
||||||
const userGroupService = new UserGroupService();
|
const userGroupService = new UserGroupService();
|
||||||
@@ -56,6 +58,17 @@
|
|||||||
axiosErrorToast(e);
|
axiosErrorToast(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateAllowedOidcClients(allowedClients: string[]) {
|
||||||
|
await userGroupService
|
||||||
|
.updateAllowedOidcClients(userGroup.id, allowedClients)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(m.allowed_oidc_clients_updated_successfully());
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
axiosErrorToast(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -110,3 +123,16 @@
|
|||||||
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleCard>
|
</CollapsibleCard>
|
||||||
|
|
||||||
|
<CollapsibleCard
|
||||||
|
id="user-group-oidc-clients"
|
||||||
|
title={m.allowed_oidc_clients()}
|
||||||
|
description={m.allowed_oidc_clients_description()}
|
||||||
|
>
|
||||||
|
<OidcClientSelection bind:selectedGroupIds={userGroup.allowedOidcClientIds} />
|
||||||
|
<div class="mt-5 flex justify-end gap-3">
|
||||||
|
<Button onclick={() => updateAllowedOidcClients(userGroup.allowedOidcClientIds)}
|
||||||
|
>{m.save()}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</CollapsibleCard>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ImageBox from '$lib/components/image-box.svelte';
|
||||||
|
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import OidcService from '$lib/services/oidc-service';
|
||||||
|
import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
|
||||||
|
import type { ListRequestOptions } from '$lib/types/list-request.type';
|
||||||
|
import type { OidcClient } from '$lib/types/oidc.type';
|
||||||
|
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
||||||
|
import { mode } from 'mode-watcher';
|
||||||
|
|
||||||
|
let {
|
||||||
|
selectedGroupIds = $bindable()
|
||||||
|
}: {
|
||||||
|
selectedGroupIds: string[];
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const oidcClientService = new OidcService();
|
||||||
|
|
||||||
|
const isLightMode = $derived(mode.current === 'light');
|
||||||
|
|
||||||
|
const columns: AdvancedTableColumn<OidcClient>[] = [
|
||||||
|
{ label: 'ID', column: 'id', hidden: true },
|
||||||
|
{ label: m.logo(), key: 'logo', cell: LogoCell },
|
||||||
|
{ label: m.name(), column: 'name', sortable: true },
|
||||||
|
{
|
||||||
|
label: m.client_launch_url(),
|
||||||
|
column: 'launchURL',
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: m.public_client(),
|
||||||
|
column: 'isPublic',
|
||||||
|
sortable: true,
|
||||||
|
hidden: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
async function fetchCallback(requestOptions: ListRequestOptions) {
|
||||||
|
const clients = await oidcClientService.listClients(requestOptions);
|
||||||
|
const unrestrictedClientIds = clients.data.filter((c) => !c.isGroupRestricted).map((c) => c.id);
|
||||||
|
selectedGroupIds = [...selectedGroupIds, ...unrestrictedClientIds];
|
||||||
|
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet LogoCell({ item }: { item: OidcClient })}
|
||||||
|
{#if item.hasLogo}
|
||||||
|
<ImageBox
|
||||||
|
class="size-12 rounded-lg"
|
||||||
|
src={cachedOidcClientLogo.getUrl(item.id, isLightMode)}
|
||||||
|
alt={m.name_logo({ name: item.name })}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="bg-muted flex size-12 items-center justify-center rounded-lg text-lg font-bold">
|
||||||
|
{item.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<AdvancedTable
|
||||||
|
id="oidc-client-selection"
|
||||||
|
{fetchCallback}
|
||||||
|
defaultSort={{ column: 'name', direction: 'asc' }}
|
||||||
|
bind:selectedIds={selectedGroupIds}
|
||||||
|
rowSelectionDisabled={(item) => !item.isGroupRestricted}
|
||||||
|
{columns}
|
||||||
|
/>
|
||||||
@@ -10,19 +10,19 @@
|
|||||||
AdvancedTableColumn,
|
AdvancedTableColumn,
|
||||||
CreateAdvancedTableActions
|
CreateAdvancedTableActions
|
||||||
} from '$lib/types/advanced-table.type';
|
} from '$lib/types/advanced-table.type';
|
||||||
import type { UserGroup, UserGroupWithUserCount } from '$lib/types/user-group.type';
|
import type { UserGroupMinimal } from '$lib/types/user-group.type';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucidePencil, LucideTrash } from '@lucide/svelte';
|
import { LucidePencil, LucideTrash } from '@lucide/svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const userGroupService = new UserGroupService();
|
const userGroupService = new UserGroupService();
|
||||||
let tableRef: AdvancedTable<UserGroupWithUserCount>;
|
let tableRef: AdvancedTable<UserGroupMinimal>;
|
||||||
|
|
||||||
export function refresh() {
|
export function refresh() {
|
||||||
return tableRef?.refresh();
|
return tableRef?.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: AdvancedTableColumn<UserGroupWithUserCount>[] = [
|
const columns: AdvancedTableColumn<UserGroupMinimal>[] = [
|
||||||
{ label: 'ID', column: 'id', hidden: true },
|
{ label: 'ID', column: 'id', hidden: true },
|
||||||
{ label: m.friendly_name(), column: 'friendlyName', sortable: true },
|
{ label: m.friendly_name(), column: 'friendlyName', sortable: true },
|
||||||
{ label: m.name(), column: 'name', sortable: true },
|
{ label: m.name(), column: 'name', sortable: true },
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
{ label: m.source(), key: 'source', hidden: !$appConfigStore.ldapEnabled, cell: SourceCell }
|
{ label: m.source(), key: 'source', hidden: !$appConfigStore.ldapEnabled, cell: SourceCell }
|
||||||
];
|
];
|
||||||
|
|
||||||
const actions: CreateAdvancedTableActions<UserGroupWithUserCount> = (group) => [
|
const actions: CreateAdvancedTableActions<UserGroupMinimal> = (group) => [
|
||||||
{
|
{
|
||||||
label: m.edit(),
|
label: m.edit(),
|
||||||
primary: true,
|
primary: true,
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
async function deleteUserGroup(userGroup: UserGroup) {
|
async function deleteUserGroup(userGroup: UserGroupMinimal) {
|
||||||
openConfirmDialog({
|
openConfirmDialog({
|
||||||
title: m.delete_name({ name: userGroup.name }),
|
title: m.delete_name({ name: userGroup.name }),
|
||||||
message: m.are_you_sure_you_want_to_delete_this_user_group(),
|
message: m.are_you_sure_you_want_to_delete_this_user_group(),
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet SourceCell({ item }: { item: UserGroupWithUserCount })}
|
{#snippet SourceCell({ item }: { item: UserGroupMinimal })}
|
||||||
<Badge class="rounded-full" variant={item.ldapId ? 'default' : 'outline'}>
|
<Badge class="rounded-full" variant={item.ldapId ? 'default' : 'outline'}>
|
||||||
{item.ldapId ? m.ldap() : m.local()}
|
{item.ldapId ? m.ldap() : m.local()}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ const config = {
|
|||||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
|
compilerOptions: {
|
||||||
|
warningFilter: (warning) => {
|
||||||
|
// Ignore "state_referenced_locally" warnings
|
||||||
|
if (warning.code === 'state_referenced_locally') return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
kit: {
|
kit: {
|
||||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import test, { expect, Page } from '@playwright/test';
|
import test, { expect, Page } from '@playwright/test';
|
||||||
import { oidcClients } from '../data';
|
import { oidcClients, userGroups } from '../data';
|
||||||
import { cleanupBackend } from '../utils/cleanup.util';
|
import { cleanupBackend } from '../utils/cleanup.util';
|
||||||
|
|
||||||
test.beforeEach(async () => await cleanupBackend());
|
test.beforeEach(async () => await cleanupBackend());
|
||||||
@@ -117,3 +117,25 @@ test('Delete OIDC client', async ({ page }) => {
|
|||||||
);
|
);
|
||||||
await expect(page.getByRole('row', { name: oidcClient.name })).not.toBeVisible();
|
await expect(page.getByRole('row', { name: oidcClient.name })).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Update OIDC client allowed user groups', async ({ page }) => {
|
||||||
|
await page.goto(`/settings/admin/oidc-clients/${oidcClients.nextcloud.id}`);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Restrict' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('row', { name: userGroups.designers.name }).getByRole('checkbox').click();
|
||||||
|
await page.getByRole('row', { name: userGroups.developers.name }).getByRole('checkbox').click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||||
|
|
||||||
|
await expect(page.getByText('Allowed user groups updated successfully')).toBeVisible();
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('row', { name: userGroups.designers.name }).getByRole('checkbox')
|
||||||
|
).toHaveAttribute('data-state', 'checked');
|
||||||
|
await expect(
|
||||||
|
page.getByRole('row', { name: userGroups.developers.name }).getByRole('checkbox')
|
||||||
|
).toHaveAttribute('data-state', 'checked');
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import test, { expect } from '@playwright/test';
|
import test, { expect } from '@playwright/test';
|
||||||
import { userGroups, users } from '../data';
|
import { oidcClients, userGroups, users } from '../data';
|
||||||
import { cleanupBackend } from '../utils/cleanup.util';
|
import { cleanupBackend } from '../utils/cleanup.util';
|
||||||
|
|
||||||
test.beforeEach(async () => await cleanupBackend());
|
test.beforeEach(async () => await cleanupBackend());
|
||||||
@@ -77,7 +77,7 @@ test('Delete user group', async ({ page }) => {
|
|||||||
test('Update user group custom claims', async ({ page }) => {
|
test('Update user group custom claims', async ({ page }) => {
|
||||||
await page.goto(`/settings/admin/user-groups/${userGroups.designers.id}`);
|
await page.goto(`/settings/admin/user-groups/${userGroups.designers.id}`);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Expand card' }).click();
|
await page.getByRole('button', { name: 'Expand card' }).first().click();
|
||||||
|
|
||||||
// Add two custom claims
|
// Add two custom claims
|
||||||
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
||||||
@@ -119,3 +119,34 @@ test('Update user group custom claims', async ({ page }) => {
|
|||||||
await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim2');
|
await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim2');
|
||||||
await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim2_value');
|
await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim2_value');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Update user group allowed user groups', async ({ page }) => {
|
||||||
|
await page.goto(`/settings/admin/user-groups/${userGroups.designers.id}`);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Expand card' }).nth(1).click();
|
||||||
|
|
||||||
|
// Unrestricted OIDC clients should be checked and disabled
|
||||||
|
const nextcloudRow = page
|
||||||
|
.getByRole('row', { name: oidcClients.nextcloud.name })
|
||||||
|
.getByRole('checkbox');
|
||||||
|
await expect(nextcloudRow).toHaveAttribute('data-state', 'checked');
|
||||||
|
await expect(nextcloudRow).toBeDisabled();
|
||||||
|
|
||||||
|
await page.getByRole('row', { name: oidcClients.tailscale.name }).getByRole('checkbox').click();
|
||||||
|
await page.getByRole('row', { name: oidcClients.immich.name }).getByRole('checkbox').click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Save' }).nth(2).click();
|
||||||
|
|
||||||
|
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||||
|
'Allowed OIDC clients updated successfully'
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('row', { name: oidcClients.tailscale.name }).getByRole('checkbox')
|
||||||
|
).toHaveAttribute('data-state', 'checked');
|
||||||
|
await expect(
|
||||||
|
page.getByRole('row', { name: oidcClients.immich.name }).getByRole('checkbox')
|
||||||
|
).toHaveAttribute('data-state', 'unchecked');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user