Skip to content

Commit

Permalink
CLOUDP-195052: Support Federated Authentication Config Generate (#3100)
Browse files Browse the repository at this point in the history
  • Loading branch information
filipcirtog authored Aug 1, 2024
1 parent 134bc2d commit a13c881
Show file tree
Hide file tree
Showing 7 changed files with 1,055 additions and 3 deletions.
45 changes: 44 additions & 1 deletion internal/kubernetes/operator/config_exporter.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 MongoDB Inc
// Copyright 2024 MongoDB Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -24,6 +24,7 @@ import (
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/dbusers"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/deployment"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/features"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/federatedauthentication"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/project"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/resources"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/streamsprocessing"
Expand All @@ -38,6 +39,7 @@ const (
yamlSeparator = "---\r\n"
maxClusters = 500
DefaultClustersCount = 10
InactiveStatus = "INACTIVE"
)

type ConfigExporter struct {
Expand Down Expand Up @@ -143,6 +145,12 @@ func (e *ConfigExporter) Run() (string, error) {
}
r = append(r, dataFederationResource...)

federatedAuthResource, err := e.exportAtlasFederatedAuth(projectName)
if err != nil {
return "", err
}
r = append(r, federatedAuthResource...)

streamProcessingResources, err := e.exportAtlasStreamProcessing(projectName)
if err != nil {
return "", err
Expand Down Expand Up @@ -400,3 +408,38 @@ func (e *ConfigExporter) exportAtlasStreamProcessing(projectName string) ([]runt

return result, nil
}

func (e *ConfigExporter) exportAtlasFederatedAuth(projectName string) ([]runtime.Object, error) {
if !e.featureValidator.IsResourceSupported(features.ResourceAtlasFederatedAuth) {
return nil, nil
}
result := make([]runtime.Object, 0)
// Gets the FederationAuthSetting
federatedAuthentificationSetting, err := e.dataProvider.FederationSetting(&admin.GetFederationSettingsApiParams{OrgId: e.orgID})
if err != nil {
return nil, fmt.Errorf("failed to retrieve federation settings: %w", err)
}
// Does not have an IdenityProvider set then no need to generate
if !federatedAuthentificationSetting.HasIdentityProviderStatus() || federatedAuthentificationSetting.GetIdentityProviderStatus() == InactiveStatus {
return nil, nil
}
// Does have an IdentityProvider and then we can generate the config
federatedAuthentification, err := federatedauthentication.BuildAtlasFederatedAuth(&federatedauthentication.AtlasFederatedAuthBuildRequest{
IncludeSecret: e.includeSecretsData,
IdentityProviderLister: e.dataProvider,
ConnectedOrgConfigsDescriber: e.dataProvider,
IdentityProviderDescriber: e.dataProvider,
ProjectStore: e.dataProvider,
ProjectID: e.projectID,
OrgID: e.orgID,
TargetNamespace: e.targetNamespace,
Version: e.operatorVersion,
Dictionary: e.dictionaryForAtlasNames,
ProjectName: projectName,
FederatedSettings: federatedAuthentificationSetting,
})
if err != nil {
return nil, fmt.Errorf("failed to export federated authentication: %w", err)
}
return append(result, federatedAuthentification), nil
}
245 changes: 244 additions & 1 deletion internal/kubernetes/operator/config_exporter_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 MongoDB Inc
// Copyright 2024 MongoDB Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -416,3 +416,246 @@ func TestExportAtlasStreamProcessing(t *testing.T) {
)
})
}

const (
legacyTestIdentityProviderID = "LegacyTestIdentityProviderID"
testIdentityProviderID = "TestIdentityProviderID"
)

var (
testProjectID = []string{"test-project-1", "test-project-2"}
secondTestProjectID = []string{"test-project-3", "test-project-4"}
testRoleProject = []string{"GROUP_OWNER", "GROUP_OWNER"}
testRoleOrganization = []string{"ORG_OWNER", "ORG_OWNER"}
testExternalGroupName = []string{"org-admin", "dev-team"}
)

func Test_ExportFederatedAuth(t *testing.T) {
testCases := []struct {
name string
setupMocks func(*mocks.MockOperatorGenericStore, *mocks.MockFeatureValidator)
expected []runtime.Object
expectedError error
}{
{
name: "should return exported resources",
setupMocks: func(store *mocks.MockOperatorGenericStore, featureValidator *mocks.MockFeatureValidator) {
legacyTestIdentityProviderID := "LegacyTestIdentityProviderID"
federationSettings := &admin.OrgFederationSettings{
Id: pointer.Get("TestFederationSettingID"),
IdentityProviderId: &legacyTestIdentityProviderID,
IdentityProviderStatus: pointer.Get("ACTIVE"),
HasRoleMappings: pointer.Get(true),
}

featureValidator.EXPECT().
IsResourceSupported(features.ResourceAtlasFederatedAuth).
Return(true)
store.EXPECT().FederationSetting(&admin.GetFederationSettingsApiParams{OrgId: orgID}).
Return(federationSettings, nil)
orgConfig := &admin.ConnectedOrgConfig{
DomainAllowList: &[]string{"example.com"},
PostAuthRoleGrants: &[]string{"ORG_OWNER"},
DomainRestrictionEnabled: true,
RoleMappings: pointer.Get(setupAuthRoleMappings(testProjectID, secondTestProjectID, testRoleProject, testRoleOrganization, testExternalGroupName, "testOrganizationID")),
IdentityProviderId: federationSettings.IdentityProviderId,
}
store.EXPECT().GetConnectedOrgConfig(&admin.GetConnectedOrgConfigApiParams{FederationSettingsId: *federationSettings.Id, OrgId: orgID}).
Return(orgConfig, nil)

identityProvider := &admin.FederationIdentityProvider{
SsoDebugEnabled: pointer.Get(true),
OktaIdpId: *federationSettings.IdentityProviderId,
Id: "TestIdentityProviderID",
}
paginatedResult := &admin.PaginatedFederationIdentityProvider{
Results: &[]admin.FederationIdentityProvider{
*identityProvider,
},
TotalCount: pointer.Get(1),
}
store.EXPECT().IdentityProviders(&admin.ListIdentityProvidersApiParams{FederationSettingsId: *federationSettings.Id}).
Return(paginatedResult, nil)

firstProject := &admin.Group{Id: pointer.Get("test-project-1"), Name: "test-project-name-1", OrgId: orgID}
secondProject := &admin.Group{Id: pointer.Get("test-project-1"), Name: "test-project-name-2", OrgId: orgID}

store.EXPECT().Project("test-project-1").Return(firstProject, nil)
store.EXPECT().Project("test-project-2").Return(secondProject, nil)
store.EXPECT().Project("test-project-3").Return(firstProject, nil)
store.EXPECT().Project("test-project-4").Return(secondProject, nil)
},
expected: []runtime.Object{
&akov2.AtlasFederatedAuth{
TypeMeta: metav1.TypeMeta{
Kind: "AtlasFederatedAuth",
APIVersion: "atlas.mongodb.com/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-project-testfederationsettingid",
Namespace: "test",
},
Spec: akov2.AtlasFederatedAuthSpec{
ConnectionSecretRef: akov2common.ResourceRefNamespaced{
Name: "my-project-credentials",
Namespace: "test",
},
Enabled: true,
DomainAllowList: []string{"example.com"},
PostAuthRoleGrants: []string{"ORG_OWNER"},
DomainRestrictionEnabled: pointer.Get(true),
SSODebugEnabled: pointer.Get(true),
RoleMappings: []akov2.RoleMapping{
{
ExternalGroupName: "org-admin",
RoleAssignments: []akov2.RoleAssignment{
{
ProjectName: "test-project-name-1",
Role: "GROUP_OWNER",
},
},
},
{
ExternalGroupName: "dev-team",
RoleAssignments: []akov2.RoleAssignment{
{
ProjectName: "test-project-name-2",
Role: "GROUP_OWNER",
},
},
},
{
ExternalGroupName: "org-admin",
RoleAssignments: []akov2.RoleAssignment{
{
Role: "ORG_OWNER",
},
{
ProjectName: "test-project-name-1",
Role: "GROUP_OWNER",
},
},
},
{
ExternalGroupName: "dev-team",
RoleAssignments: []akov2.RoleAssignment{
{
Role: "ORG_OWNER",
},
{
ProjectName: "test-project-name-2",
Role: "GROUP_OWNER",
},
},
},
},
},
Status: akov2status.AtlasFederatedAuthStatus{
Common: akoapi.Common{
Conditions: []akoapi.Condition{},
},
},
},
},
expectedError: nil,
},
{
name: "should return nothing because the IDP is not active",
setupMocks: func(store *mocks.MockOperatorGenericStore, featureValidator *mocks.MockFeatureValidator) {
federationSettings := &admin.OrgFederationSettings{
Id: pointer.Get("TestFederationSettingID"),
IdentityProviderStatus: pointer.Get("INACTIVE"),
IdentityProviderId: pointer.Get(legacyTestIdentityProviderID),
HasRoleMappings: pointer.Get(false),
}
featureValidator.EXPECT().
IsResourceSupported(features.ResourceAtlasFederatedAuth).
Return(true)
store.EXPECT().FederationSetting(&admin.GetFederationSettingsApiParams{OrgId: orgID}).
Return(federationSettings, nil)
},
expected: nil,
expectedError: nil,
},
{
name: "should return nothing because the IDP is not present",
setupMocks: func(store *mocks.MockOperatorGenericStore, featureValidator *mocks.MockFeatureValidator) {
federationSettings := &admin.OrgFederationSettings{
Id: pointer.Get("TestFederationSettingID"),
HasRoleMappings: pointer.Get(false),
}
featureValidator.EXPECT().
IsResourceSupported(features.ResourceAtlasFederatedAuth).
Return(true)
store.EXPECT().FederationSetting(&admin.GetFederationSettingsApiParams{OrgId: orgID}).
Return(federationSettings, nil)
},
expected: nil,
expectedError: nil,
},
{
name: "should return nil when resource is not supported",
setupMocks: func(_ *mocks.MockOperatorGenericStore, featureValidator *mocks.MockFeatureValidator) {
featureValidator.EXPECT().
IsResourceSupported(features.ResourceAtlasFederatedAuth).
Return(false)
},
expected: nil,
expectedError: nil,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctl := gomock.NewController(t)
atlasOperatorGenericStore := mocks.NewMockOperatorGenericStore(ctl)
featureValidator := mocks.NewMockFeatureValidator(ctl)
ce := defaultTestConfigExporter(t, atlasOperatorGenericStore, featureValidator)
defer ctl.Finish()
tc.setupMocks(atlasOperatorGenericStore, featureValidator)

resources, err := ce.exportAtlasFederatedAuth("my-project")
require.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expected, resources)
})
}
}
func defaultTestConfigExporter(t *testing.T, genStore *mocks.MockOperatorGenericStore, featureValidator *mocks.MockFeatureValidator) *ConfigExporter {
t.Helper()
return NewConfigExporter(genStore, nil, projectID, orgID).
WithTargetNamespace("test").
WithFeatureValidator(featureValidator).
WithTargetOperatorVersion("2.4.0").
WithSecretsData(true)
}

func setupAuthRoleMappings(testProjectID, secondTestProjectID, testRoleProject, testRoleOrganization, testExternalGroupName []string, testOrganizationID string) []admin.AuthFederationRoleMapping {
AuthRoleMappings := make([]admin.AuthFederationRoleMapping, len(testRoleProject)+len(testRoleOrganization))
for i := range testProjectID {
AuthRoleMappings[i] = admin.AuthFederationRoleMapping{
ExternalGroupName: testExternalGroupName[i],
RoleAssignments: &[]admin.RoleAssignment{
{
GroupId: &testProjectID[i],
Role: &testRoleProject[i],
},
},
}
}
for i := range testRoleOrganization {
AuthRoleMappings[len(testProjectID)+i] = admin.AuthFederationRoleMapping{
ExternalGroupName: testExternalGroupName[i],
RoleAssignments: &[]admin.RoleAssignment{
{
OrgId: &testOrganizationID,
Role: &testRoleOrganization[i],
},
{
GroupId: &secondTestProjectID[i],
Role: &testRoleProject[i],
},
},
}
}
return AuthRoleMappings
}
Loading

0 comments on commit a13c881

Please sign in to comment.