633 lines
23 KiB
Go
633 lines
23 KiB
Go
|
|
package queryapi
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"queryorchestration/internal/cognitoauth"
|
||
|
|
"queryorchestration/internal/usermanagement"
|
||
|
|
|
||
|
|
"github.com/labstack/echo/v4"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestAdminModeForRoles(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
roles []string
|
||
|
|
want adminTeamMode
|
||
|
|
}{
|
||
|
|
{name: "super admin wins", roles: []string{"user_admin", "super_admin"}, want: adminTeamModeSuperAdmin},
|
||
|
|
{name: "client admin maps to user admin", roles: []string{"client_admin"}, want: adminTeamModeUserAdmin},
|
||
|
|
{name: "auditor without higher role", roles: []string{"auditor"}, want: adminTeamModeAuditor},
|
||
|
|
{name: "unrecognized", roles: []string{"client_user"}, want: adminTeamModeNone},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
if got := adminModeForRoles(tt.roles); got != tt.want {
|
||
|
|
t.Fatalf("adminModeForRoles(%v) = %s, want %s", tt.roles, got, tt.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIsAAreteEmail(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
email string
|
||
|
|
want bool
|
||
|
|
}{
|
||
|
|
{email: "person@aarete.com", want: true},
|
||
|
|
{email: "Person@AArete.com", want: true},
|
||
|
|
{email: "person@sub.aarete.com", want: false},
|
||
|
|
{email: "person@aarete.com.fake", want: false},
|
||
|
|
{email: "not-an-email", want: false},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.email, func(t *testing.T) {
|
||
|
|
if got := isAAreteEmail(tt.email); got != tt.want {
|
||
|
|
t.Fatalf("isAAreteEmail(%q) = %v, want %v", tt.email, got, tt.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAdminTargetVisibilityAndMutation(t *testing.T) {
|
||
|
|
clientRoles := []string{"client_user"}
|
||
|
|
userAdminRoles := []string{"user_admin"}
|
||
|
|
auditorRoles := []string{"auditor"}
|
||
|
|
superAdminRoles := []string{"super_admin"}
|
||
|
|
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
mode adminTeamMode
|
||
|
|
email string
|
||
|
|
roles []string
|
||
|
|
wantRead bool
|
||
|
|
wantMutate bool
|
||
|
|
}{
|
||
|
|
{name: "user admin can read and mutate client user", mode: adminTeamModeUserAdmin, email: "client@example.com", roles: clientRoles, wantRead: true, wantMutate: true},
|
||
|
|
{name: "user admin can read and mutate user admin", mode: adminTeamModeUserAdmin, email: "admin@example.com", roles: userAdminRoles, wantRead: true, wantMutate: true},
|
||
|
|
{name: "user admin cannot read or mutate aarete domain", mode: adminTeamModeUserAdmin, email: "person@aarete.com", roles: clientRoles, wantRead: false, wantMutate: false},
|
||
|
|
{name: "user admin cannot read or mutate auditor", mode: adminTeamModeUserAdmin, email: "auditor@example.com", roles: auditorRoles, wantRead: false, wantMutate: false},
|
||
|
|
{name: "user admin cannot read or mutate super admin", mode: adminTeamModeUserAdmin, email: "super@example.com", roles: superAdminRoles, wantRead: false, wantMutate: false},
|
||
|
|
{name: "auditor can read all", mode: adminTeamModeAuditor, email: "person@aarete.com", roles: superAdminRoles, wantRead: true, wantMutate: false},
|
||
|
|
{name: "super admin can read and mutate all", mode: adminTeamModeSuperAdmin, email: "person@aarete.com", roles: superAdminRoles, wantRead: true, wantMutate: true},
|
||
|
|
{name: "none cannot read or mutate", mode: adminTeamModeNone, email: "client@example.com", roles: clientRoles, wantRead: false, wantMutate: false},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
if got := canAdminModeReadTarget(tt.mode, tt.email, tt.roles); got != tt.wantRead {
|
||
|
|
t.Fatalf("canAdminModeReadTarget() = %v, want %v", got, tt.wantRead)
|
||
|
|
}
|
||
|
|
if got := canAdminModeMutateTarget(tt.mode, tt.email, tt.roles); got != tt.wantMutate {
|
||
|
|
t.Fatalf("canAdminModeMutateTarget() = %v, want %v", got, tt.wantMutate)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAdminCreatePermissions(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
mode adminTeamMode
|
||
|
|
email string
|
||
|
|
roles []string
|
||
|
|
want bool
|
||
|
|
}{
|
||
|
|
{name: "super admin can create all roles", mode: adminTeamModeSuperAdmin, email: "person@aarete.com", roles: []string{"super_admin"}, want: true},
|
||
|
|
{name: "user admin can create client user", mode: adminTeamModeUserAdmin, email: "client@example.com", roles: []string{"client_user"}, want: true},
|
||
|
|
{name: "user admin can create user admin", mode: adminTeamModeUserAdmin, email: "admin@example.com", roles: []string{"client_user", "user_admin"}, want: true},
|
||
|
|
{name: "user admin cannot create aarete domain", mode: adminTeamModeUserAdmin, email: "person@aarete.com", roles: []string{"client_user"}, want: false},
|
||
|
|
{name: "user admin cannot create auditor", mode: adminTeamModeUserAdmin, email: "auditor@example.com", roles: []string{"auditor"}, want: false},
|
||
|
|
{name: "auditor cannot create", mode: adminTeamModeAuditor, email: "client@example.com", roles: []string{"client_user"}, want: false},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
if got := canAdminModeCreateUser(tt.mode, tt.email, tt.roles); got != tt.want {
|
||
|
|
t.Fatalf("canAdminModeCreateUser() = %v, want %v", got, tt.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUserAdminCanAssignRoles(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
roles []string
|
||
|
|
want bool
|
||
|
|
}{
|
||
|
|
{name: "client user", roles: []string{"client_user"}, want: true},
|
||
|
|
{name: "user admin", roles: []string{"user_admin"}, want: true},
|
||
|
|
{name: "client user and user admin", roles: []string{"client_user", "user_admin"}, want: true},
|
||
|
|
{name: "auditor rejected", roles: []string{"auditor"}, want: false},
|
||
|
|
{name: "super admin rejected", roles: []string{"super_admin"}, want: false},
|
||
|
|
{name: "unknown role rejected", roles: []string{"custom_role"}, want: false},
|
||
|
|
{name: "mixed allowed and protected rejected", roles: []string{"client_user", "auditor"}, want: false},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
if got := userAdminCanAssignRoles(tt.roles); got != tt.want {
|
||
|
|
t.Fatalf("userAdminCanAssignRoles(%v) = %v, want %v", tt.roles, got, tt.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCanAdminModeAssignRequestedRoles(t *testing.T) {
|
||
|
|
auditorRoles := []string{"auditor"}
|
||
|
|
clientRoles := []string{"client_user"}
|
||
|
|
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
mode adminTeamMode
|
||
|
|
roles *[]string
|
||
|
|
want bool
|
||
|
|
}{
|
||
|
|
{name: "nil roles allowed", mode: adminTeamModeUserAdmin, roles: nil, want: true},
|
||
|
|
{name: "user admin can assign client role", mode: adminTeamModeUserAdmin, roles: &clientRoles, want: true},
|
||
|
|
{name: "user admin cannot assign auditor role", mode: adminTeamModeUserAdmin, roles: &auditorRoles, want: false},
|
||
|
|
{name: "super admin can assign auditor role", mode: adminTeamModeSuperAdmin, roles: &auditorRoles, want: true},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
if got := canAdminModeAssignRequestedRoles(tt.mode, tt.roles); got != tt.want {
|
||
|
|
t.Fatalf("canAdminModeAssignRequestedRoles() = %v, want %v", got, tt.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGetAdminTeamMode(t *testing.T) {
|
||
|
|
server := newAdminListPermitServer(t)
|
||
|
|
defer server.Close()
|
||
|
|
|
||
|
|
ctrl := NewControllers(&Services{}, &mockAuthConfig{})
|
||
|
|
permitConfig := &usermanagement.PermitConfig{
|
||
|
|
APIKey: "test-api-key",
|
||
|
|
BaseURL: server.URL,
|
||
|
|
}
|
||
|
|
|
||
|
|
t.Run("disabled auth acts as super admin", func(t *testing.T) {
|
||
|
|
t.Setenv("DISABLE_AUTH", "true")
|
||
|
|
ctx, _ := newAdminAccessTestContext(http.MethodGet)
|
||
|
|
|
||
|
|
if got := ctrl.getAdminTeamMode(ctx, server.Client(), permitConfig); got != adminTeamModeSuperAdmin {
|
||
|
|
t.Fatalf("getAdminTeamMode() = %s, want %s", got, adminTeamModeSuperAdmin)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("permit subject roles determine mode", func(t *testing.T) {
|
||
|
|
t.Setenv("DISABLE_AUTH", "")
|
||
|
|
ctx, _ := newAdminAccessTestContext(http.MethodGet)
|
||
|
|
ctx.Set("user_claims", map[string]interface{}{"sub": "user-admin-sub"})
|
||
|
|
|
||
|
|
if got := ctrl.getAdminTeamMode(ctx, server.Client(), permitConfig); got != adminTeamModeUserAdmin {
|
||
|
|
t.Fatalf("getAdminTeamMode() = %s, want %s", got, adminTeamModeUserAdmin)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("context groups are used when subject is unavailable", func(t *testing.T) {
|
||
|
|
t.Setenv("DISABLE_AUTH", "")
|
||
|
|
ctx, _ := newAdminAccessTestContext(http.MethodGet)
|
||
|
|
ctx.Set("user_info", cognitoauth.UserInfo{Groups: []string{"auditor"}})
|
||
|
|
|
||
|
|
if got := ctrl.getAdminTeamMode(ctx, server.Client(), permitConfig); got != adminTeamModeAuditor {
|
||
|
|
t.Fatalf("getAdminTeamMode() = %s, want %s", got, adminTeamModeAuditor)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("no auth context has no admin mode", func(t *testing.T) {
|
||
|
|
t.Setenv("DISABLE_AUTH", "")
|
||
|
|
ctx, _ := newAdminAccessTestContext(http.MethodGet)
|
||
|
|
|
||
|
|
if got := ctrl.getAdminTeamMode(ctx, server.Client(), permitConfig); got != adminTeamModeNone {
|
||
|
|
t.Fatalf("getAdminTeamMode() = %s, want %s", got, adminTeamModeNone)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGetAdminTargetAccess(t *testing.T) {
|
||
|
|
server := newAdminListPermitServer(t)
|
||
|
|
defer server.Close()
|
||
|
|
t.Setenv("DISABLE_AUTH", "false")
|
||
|
|
t.Setenv("PERMIT_IO_BASE_URL", server.URL)
|
||
|
|
|
||
|
|
ctrl := NewControllers(&Services{}, &mockAuthConfig{})
|
||
|
|
|
||
|
|
t.Run("loads target roles and caller mode", func(t *testing.T) {
|
||
|
|
ctx, _ := newAdminAccessTestContext(http.MethodGet)
|
||
|
|
ctx.Set("user_claims", map[string]interface{}{"sub": "user-admin-sub"})
|
||
|
|
cognitoUser := &usermanagement.CognitoUserResponse{
|
||
|
|
SubjectID: "client-sub",
|
||
|
|
Email: "client@example.com",
|
||
|
|
}
|
||
|
|
|
||
|
|
access := ctrl.getAdminTargetAccess(ctx, UserEmail(cognitoUser.Email), cognitoUser)
|
||
|
|
if access.mode != adminTeamModeUserAdmin {
|
||
|
|
t.Fatalf("getAdminTargetAccess().mode = %s, want %s", access.mode, adminTeamModeUserAdmin)
|
||
|
|
}
|
||
|
|
if !access.roleLookupOK {
|
||
|
|
t.Fatalf("getAdminTargetAccess().roleLookupOK = false, want true")
|
||
|
|
}
|
||
|
|
if len(access.roles) != 1 || access.roles[0] != "client_user" {
|
||
|
|
t.Fatalf("getAdminTargetAccess().roles = %v, want [client_user]", access.roles)
|
||
|
|
}
|
||
|
|
if !access.canRead(cognitoUser.Email) {
|
||
|
|
t.Fatalf("adminTargetAccess.canRead() = false, want true")
|
||
|
|
}
|
||
|
|
if !access.canMutate(cognitoUser.Email) {
|
||
|
|
t.Fatalf("adminTargetAccess.canMutate() = false, want true")
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("user admin fail closes when target role lookup fails", func(t *testing.T) {
|
||
|
|
ctx, _ := newAdminAccessTestContext(http.MethodPatch)
|
||
|
|
ctx.Set("user_claims", map[string]interface{}{"sub": "user-admin-sub"})
|
||
|
|
cognitoUser := &usermanagement.CognitoUserResponse{
|
||
|
|
SubjectID: "role-error-sub",
|
||
|
|
Email: "client@example.com",
|
||
|
|
}
|
||
|
|
|
||
|
|
access := ctrl.getAdminTargetAccess(ctx, UserEmail(cognitoUser.Email), cognitoUser)
|
||
|
|
if access.mode != adminTeamModeUserAdmin {
|
||
|
|
t.Fatalf("getAdminTargetAccess().mode = %s, want %s", access.mode, adminTeamModeUserAdmin)
|
||
|
|
}
|
||
|
|
if access.roleLookupOK {
|
||
|
|
t.Fatalf("getAdminTargetAccess().roleLookupOK = true, want false")
|
||
|
|
}
|
||
|
|
if access.canRead(cognitoUser.Email) {
|
||
|
|
t.Fatalf("adminTargetAccess.canRead() = true, want false")
|
||
|
|
}
|
||
|
|
if access.canMutate(cognitoUser.Email) {
|
||
|
|
t.Fatalf("adminTargetAccess.canMutate() = true, want false")
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAdminUserErrorResponses(t *testing.T) {
|
||
|
|
ctx, rec := newAdminAccessTestContext(http.MethodPatch)
|
||
|
|
if err := adminUserForbidden(ctx); err != nil {
|
||
|
|
t.Fatalf("adminUserForbidden() error = %v", err)
|
||
|
|
}
|
||
|
|
if rec.Code != http.StatusForbidden {
|
||
|
|
t.Fatalf("adminUserForbidden() status = %d, want %d", rec.Code, http.StatusForbidden)
|
||
|
|
}
|
||
|
|
if !strings.Contains(rec.Body.String(), "Insufficient permissions") {
|
||
|
|
t.Fatalf("adminUserForbidden() body = %q, want permissions message", rec.Body.String())
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx, rec = newAdminAccessTestContext(http.MethodGet)
|
||
|
|
if err := adminUserNotFound(ctx, "hidden@example.com"); err != nil {
|
||
|
|
t.Fatalf("adminUserNotFound() error = %v", err)
|
||
|
|
}
|
||
|
|
if rec.Code != http.StatusNotFound {
|
||
|
|
t.Fatalf("adminUserNotFound() status = %d, want %d", rec.Code, http.StatusNotFound)
|
||
|
|
}
|
||
|
|
if !strings.Contains(rec.Body.String(), "User not found: hidden@example.com") {
|
||
|
|
t.Fatalf("adminUserNotFound() body = %q, want not found message", rec.Body.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAdminUserListPaginationAndPage(t *testing.T) {
|
||
|
|
pageInput := int32(2)
|
||
|
|
pageSizeInput := int32(2)
|
||
|
|
page, pageSize := adminUserListPagination(ListAdminUsersParams{
|
||
|
|
Page: &pageInput,
|
||
|
|
PageSize: &pageSizeInput,
|
||
|
|
})
|
||
|
|
if page != 2 || pageSize != 2 {
|
||
|
|
t.Fatalf("adminUserListPagination() = (%d, %d), want (2, 2)", page, pageSize)
|
||
|
|
}
|
||
|
|
|
||
|
|
tooLargePageSize := int32(100)
|
||
|
|
_, defaultedPageSize := adminUserListPagination(ListAdminUsersParams{PageSize: &tooLargePageSize})
|
||
|
|
if defaultedPageSize != 50 {
|
||
|
|
t.Fatalf("adminUserListPagination() pageSize = %d, want default 50", defaultedPageSize)
|
||
|
|
}
|
||
|
|
|
||
|
|
users := []AdminUserDetails{
|
||
|
|
{CognitoSubjectId: "sub-1"},
|
||
|
|
{CognitoSubjectId: "sub-2"},
|
||
|
|
{CognitoSubjectId: "sub-3"},
|
||
|
|
}
|
||
|
|
pageUsers, hasMore := paginateAdminUsers(users, 1, 2)
|
||
|
|
if len(pageUsers) != 2 || !hasMore {
|
||
|
|
t.Fatalf("paginateAdminUsers() = %d users, hasMore %v; want 2 users and hasMore true", len(pageUsers), hasMore)
|
||
|
|
}
|
||
|
|
|
||
|
|
pageUsers, hasMore = paginateAdminUsers(users, 2, 2)
|
||
|
|
if len(pageUsers) != 1 || hasMore {
|
||
|
|
t.Fatalf("paginateAdminUsers(page 2) = %d users, hasMore %v; want 1 user and hasMore false", len(pageUsers), hasMore)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCollectAdminCognitoUsersStopsAtUserCap(t *testing.T) {
|
||
|
|
nextToken := "next"
|
||
|
|
_, err := collectAdminCognitoUsers(func(pageSize int, paginationToken *string) (*usermanagement.ListCognitoUsersResult, error) {
|
||
|
|
users := make([]usermanagement.CognitoUserResponse, pageSize+1)
|
||
|
|
for i := range users {
|
||
|
|
users[i] = usermanagement.CognitoUserResponse{SubjectID: "sub"}
|
||
|
|
}
|
||
|
|
return &usermanagement.ListCognitoUsersResult{Users: users, PaginationKey: &nextToken}, nil
|
||
|
|
})
|
||
|
|
|
||
|
|
if err == nil || !strings.Contains(err.Error(), "exceeded 6000 users") {
|
||
|
|
t.Fatalf("collectAdminCognitoUsers() error = %v, want user cap error", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCollectAdminCognitoUsersStopsAtPageCap(t *testing.T) {
|
||
|
|
nextToken := "next"
|
||
|
|
_, err := collectAdminCognitoUsers(func(pageSize int, paginationToken *string) (*usermanagement.ListCognitoUsersResult, error) {
|
||
|
|
return &usermanagement.ListCognitoUsersResult{
|
||
|
|
Users: []usermanagement.CognitoUserResponse{{SubjectID: "sub"}},
|
||
|
|
PaginationKey: &nextToken,
|
||
|
|
}, nil
|
||
|
|
})
|
||
|
|
|
||
|
|
if err == nil || !strings.Contains(err.Error(), "exceeded 100 pages") {
|
||
|
|
t.Fatalf("collectAdminCognitoUsers() error = %v, want page cap error", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestVisibleAdminUsersAppliesDEVOPS722Visibility(t *testing.T) {
|
||
|
|
users := []usermanagement.CognitoUserResponse{
|
||
|
|
{SubjectID: "client-sub", Email: "client@example.com", FirstName: "Client", LastName: "User", Status: "CONFIRMED", Enabled: true},
|
||
|
|
{SubjectID: "user-admin-sub", Email: "admin@example.com", FirstName: "User", LastName: "Admin", Status: "CONFIRMED", Enabled: true},
|
||
|
|
{SubjectID: "aarete-sub", Email: "person@aarete.com", FirstName: "AArete", LastName: "User", Status: "CONFIRMED", Enabled: true},
|
||
|
|
{SubjectID: "auditor-sub", Email: "auditor@example.com", FirstName: "Audit", LastName: "User", Status: "CONFIRMED", Enabled: true},
|
||
|
|
{SubjectID: "super-sub", Email: "super@example.com", FirstName: "Super", LastName: "User", Status: "CONFIRMED", Enabled: true},
|
||
|
|
{SubjectID: "missing-role-sub", Email: "missing-role@example.com", FirstName: "Missing", LastName: "Role", Status: "CONFIRMED", Enabled: true},
|
||
|
|
}
|
||
|
|
rolesBySubject := map[string][]string{
|
||
|
|
"client-sub": {"client_user"},
|
||
|
|
"user-admin-sub": {"user_admin"},
|
||
|
|
"aarete-sub": {"client_user"},
|
||
|
|
"auditor-sub": {"auditor"},
|
||
|
|
"super-sub": {"super_admin"},
|
||
|
|
}
|
||
|
|
|
||
|
|
ctrl := NewControllers(&Services{}, &mockAuthConfig{})
|
||
|
|
userAdminVisible := ctrl.visibleAdminUsers(users, adminTeamModeUserAdmin, rolesBySubject)
|
||
|
|
assertVisibleSubjects(t, userAdminVisible, []string{"client-sub", "user-admin-sub"})
|
||
|
|
|
||
|
|
auditorVisible := ctrl.visibleAdminUsers(users, adminTeamModeAuditor, rolesBySubject)
|
||
|
|
assertVisibleSubjects(t, auditorVisible, []string{"client-sub", "user-admin-sub", "aarete-sub", "auditor-sub", "super-sub", "missing-role-sub"})
|
||
|
|
|
||
|
|
superAdminVisible := ctrl.visibleAdminUsers(users, adminTeamModeSuperAdmin, rolesBySubject)
|
||
|
|
assertVisibleSubjects(t, superAdminVisible, []string{"client-sub", "user-admin-sub", "aarete-sub", "auditor-sub", "super-sub", "missing-role-sub"})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAdminRoleAssignmentsByUserGroupsRoles(t *testing.T) {
|
||
|
|
got := adminRoleAssignmentsByUser([]usermanagement.RoleAssignment{
|
||
|
|
{User: "sub-1", Role: "client_user", Tenant: "default"},
|
||
|
|
{User: "sub-1", Role: "user_admin", Tenant: "default"},
|
||
|
|
{User: "sub-2", Role: "auditor", Tenant: "default"},
|
||
|
|
{User: "", Role: "super_admin", Tenant: "default"},
|
||
|
|
{User: "sub-3", Role: "", Tenant: "default"},
|
||
|
|
})
|
||
|
|
|
||
|
|
assertVisibleRoleMap(t, got, map[string][]string{
|
||
|
|
"sub-1": {"client_user", "user_admin"},
|
||
|
|
"sub-2": {"auditor"},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAdminUserDetailsRolesMarshalAsEmptyArray(t *testing.T) {
|
||
|
|
detail := adminUserDetailsFromCognito(usermanagement.CognitoUserResponse{
|
||
|
|
SubjectID: "sub-without-roles",
|
||
|
|
Email: "client@example.com",
|
||
|
|
}, nil)
|
||
|
|
|
||
|
|
body, err := json.Marshal(detail)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("marshal admin user details: %v", err)
|
||
|
|
}
|
||
|
|
if strings.Contains(string(body), `"roles":null`) {
|
||
|
|
t.Fatalf("admin user details marshaled roles as null: %s", body)
|
||
|
|
}
|
||
|
|
if !strings.Contains(string(body), `"roles":[]`) {
|
||
|
|
t.Fatalf("admin user details missing empty roles array: %s", body)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUpdateUserRolesInPermitEmptyArrayClearsRoles(t *testing.T) {
|
||
|
|
var deletedRoles []string
|
||
|
|
|
||
|
|
mux := http.NewServeMux()
|
||
|
|
mux.HandleFunc("/v2/api-key/scope", func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
if err := json.NewEncoder(w).Encode(map[string]string{
|
||
|
|
"organization_id": "test-org",
|
||
|
|
"project_id": "test-project",
|
||
|
|
"environment_id": "test-env",
|
||
|
|
}); err != nil {
|
||
|
|
t.Fatalf("encode scope response: %v", err)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
mux.HandleFunc("/v2/facts/test-project/test-env/role_assignments", func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
switch r.Method {
|
||
|
|
case http.MethodGet:
|
||
|
|
if err := json.NewEncoder(w).Encode([]map[string]string{
|
||
|
|
{"role": "client_user", "user": "target-sub", "tenant": "default"},
|
||
|
|
{"role": "user_admin", "user": "target-sub", "tenant": "default"},
|
||
|
|
}); err != nil {
|
||
|
|
t.Fatalf("encode role assignments: %v", err)
|
||
|
|
}
|
||
|
|
case http.MethodDelete:
|
||
|
|
var assignment usermanagement.RoleAssignment
|
||
|
|
if err := json.NewDecoder(r.Body).Decode(&assignment); err != nil {
|
||
|
|
t.Fatalf("decode delete role assignment: %v", err)
|
||
|
|
}
|
||
|
|
deletedRoles = append(deletedRoles, assignment.Role)
|
||
|
|
w.WriteHeader(http.StatusNoContent)
|
||
|
|
default:
|
||
|
|
t.Fatalf("unexpected method %s", r.Method)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
roleServer := httptest.NewServer(mux)
|
||
|
|
defer roleServer.Close()
|
||
|
|
|
||
|
|
ctrl := NewControllers(&Services{}, &mockAuthConfig{})
|
||
|
|
ctx, _ := newAdminAccessTestContext(http.MethodPatch)
|
||
|
|
emptyRoles := []string{}
|
||
|
|
ctrl.updateUserRolesInPermit(
|
||
|
|
ctx,
|
||
|
|
UserEmail("target@example.com"),
|
||
|
|
AdminUserUpdate{Roles: &emptyRoles},
|
||
|
|
cognitoauth.UserInfo{Email: "admin@example.com"},
|
||
|
|
"target-sub",
|
||
|
|
roleServer.Client(),
|
||
|
|
&usermanagement.PermitConfig{APIKey: "test-api-key", BaseURL: roleServer.URL},
|
||
|
|
)
|
||
|
|
|
||
|
|
assertVisibleRoleMap(t, map[string][]string{"deleted": deletedRoles}, map[string][]string{
|
||
|
|
"deleted": {"client_user", "user_admin"},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUpdateUserRolesInPermitAddsAndRemovesRoles(t *testing.T) {
|
||
|
|
var assignedRoles []string
|
||
|
|
var deletedRoles []string
|
||
|
|
|
||
|
|
mux := http.NewServeMux()
|
||
|
|
mux.HandleFunc("/v2/api-key/scope", func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
if err := json.NewEncoder(w).Encode(map[string]string{
|
||
|
|
"organization_id": "test-org",
|
||
|
|
"project_id": "test-project",
|
||
|
|
"environment_id": "test-env",
|
||
|
|
}); err != nil {
|
||
|
|
t.Fatalf("encode scope response: %v", err)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
mux.HandleFunc("/v2/facts/test-project/test-env/role_assignments", func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
switch r.Method {
|
||
|
|
case http.MethodGet:
|
||
|
|
if err := json.NewEncoder(w).Encode([]map[string]string{
|
||
|
|
{"role": "client_user", "user": "target-sub", "tenant": "default"},
|
||
|
|
{"role": "user_admin", "user": "target-sub", "tenant": "default"},
|
||
|
|
}); err != nil {
|
||
|
|
t.Fatalf("encode role assignments: %v", err)
|
||
|
|
}
|
||
|
|
case http.MethodPost:
|
||
|
|
var assignment usermanagement.RoleAssignment
|
||
|
|
if err := json.NewDecoder(r.Body).Decode(&assignment); err != nil {
|
||
|
|
t.Fatalf("decode assign role assignment: %v", err)
|
||
|
|
}
|
||
|
|
assignedRoles = append(assignedRoles, assignment.Role)
|
||
|
|
w.WriteHeader(http.StatusCreated)
|
||
|
|
case http.MethodDelete:
|
||
|
|
var assignment usermanagement.RoleAssignment
|
||
|
|
if err := json.NewDecoder(r.Body).Decode(&assignment); err != nil {
|
||
|
|
t.Fatalf("decode delete role assignment: %v", err)
|
||
|
|
}
|
||
|
|
deletedRoles = append(deletedRoles, assignment.Role)
|
||
|
|
w.WriteHeader(http.StatusNoContent)
|
||
|
|
default:
|
||
|
|
t.Fatalf("unexpected method %s", r.Method)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
roleServer := httptest.NewServer(mux)
|
||
|
|
defer roleServer.Close()
|
||
|
|
|
||
|
|
ctrl := NewControllers(&Services{}, &mockAuthConfig{})
|
||
|
|
ctx, _ := newAdminAccessTestContext(http.MethodPatch)
|
||
|
|
requestedRoles := []string{"client_user", "auditor"}
|
||
|
|
ctrl.updateUserRolesInPermit(
|
||
|
|
ctx,
|
||
|
|
UserEmail("target@example.com"),
|
||
|
|
AdminUserUpdate{Roles: &requestedRoles},
|
||
|
|
cognitoauth.UserInfo{Email: "admin@example.com"},
|
||
|
|
"target-sub",
|
||
|
|
roleServer.Client(),
|
||
|
|
&usermanagement.PermitConfig{APIKey: "test-api-key", BaseURL: roleServer.URL},
|
||
|
|
)
|
||
|
|
|
||
|
|
assertVisibleRoleMap(t, map[string][]string{
|
||
|
|
"assigned": assignedRoles,
|
||
|
|
"deleted": deletedRoles,
|
||
|
|
}, map[string][]string{
|
||
|
|
"assigned": {"auditor"},
|
||
|
|
"deleted": {"user_admin"},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func newAdminListPermitServer(t *testing.T) *httptest.Server {
|
||
|
|
t.Helper()
|
||
|
|
|
||
|
|
mux := http.NewServeMux()
|
||
|
|
mux.HandleFunc("/v2/api-key/scope", func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
if err := json.NewEncoder(w).Encode(map[string]string{
|
||
|
|
"organization_id": "test-org",
|
||
|
|
"project_id": "test-project",
|
||
|
|
"environment_id": "test-env",
|
||
|
|
}); err != nil {
|
||
|
|
t.Fatalf("encode scope response: %v", err)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
mux.HandleFunc("/v2/facts/test-project/test-env/role_assignments", func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := r.URL.Query().Get("user")
|
||
|
|
if user == "role-error-sub" {
|
||
|
|
http.Error(w, "permit unavailable", http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
roleByUser := map[string]string{
|
||
|
|
"client-sub": "client_user",
|
||
|
|
"user-admin-sub": "user_admin",
|
||
|
|
"aarete-sub": "client_user",
|
||
|
|
"auditor-sub": "auditor",
|
||
|
|
"super-sub": "super_admin",
|
||
|
|
}
|
||
|
|
role := roleByUser[user]
|
||
|
|
assignments := []map[string]string{}
|
||
|
|
if role != "" {
|
||
|
|
assignments = append(assignments, map[string]string{"role": role, "user": user, "tenant": "default"})
|
||
|
|
}
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
if err := json.NewEncoder(w).Encode(assignments); err != nil {
|
||
|
|
t.Fatalf("encode role assignments: %v", err)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
return httptest.NewServer(mux)
|
||
|
|
}
|
||
|
|
|
||
|
|
func newAdminAccessTestContext(method string) (echo.Context, *httptest.ResponseRecorder) {
|
||
|
|
e := echo.New()
|
||
|
|
req := httptest.NewRequest(method, "/admin/users", nil)
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
return e.NewContext(req, rec), rec
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertVisibleSubjects(t *testing.T, users []AdminUserDetails, want []string) {
|
||
|
|
t.Helper()
|
||
|
|
|
||
|
|
got := make([]string, 0, len(users))
|
||
|
|
for _, user := range users {
|
||
|
|
got = append(got, user.CognitoSubjectId)
|
||
|
|
}
|
||
|
|
if len(got) != len(want) {
|
||
|
|
t.Fatalf("visible subjects = %v, want %v", got, want)
|
||
|
|
}
|
||
|
|
for i := range want {
|
||
|
|
if got[i] != want[i] {
|
||
|
|
t.Fatalf("visible subjects = %v, want %v", got, want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertVisibleRoleMap(t *testing.T, got map[string][]string, want map[string][]string) {
|
||
|
|
t.Helper()
|
||
|
|
|
||
|
|
if len(got) != len(want) {
|
||
|
|
t.Fatalf("role map = %v, want %v", got, want)
|
||
|
|
}
|
||
|
|
for subject, wantRoles := range want {
|
||
|
|
gotRoles, ok := got[subject]
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("role map missing subject %q: %v", subject, got)
|
||
|
|
}
|
||
|
|
if len(gotRoles) != len(wantRoles) {
|
||
|
|
t.Fatalf("role map[%q] = %v, want %v", subject, gotRoles, wantRoles)
|
||
|
|
}
|
||
|
|
for i := range wantRoles {
|
||
|
|
if gotRoles[i] != wantRoles[i] {
|
||
|
|
t.Fatalf("role map[%q] = %v, want %v", subject, gotRoles, wantRoles)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|