Skip to content

Commit 6ede3bc

Browse files
UndermyspellDenisBiondic
authored andcommitted
feat(login) add support for user- and system assigned managed identity for azure login
1 parent e9d838d commit 6ede3bc

File tree

3 files changed

+140
-19
lines changed

3 files changed

+140
-19
lines changed

pkg/recipes/azure_login/factory.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,27 @@ import (
1010
// - service-principal-id
1111
// - service-principal-secret
1212
// - service-principal-tenant
13+
// - user-assigned-managed-identity-client-id
14+
// - use-managed-identity
1315
func New(executor commands.Executor) *Login {
1416
return &Login{
15-
servicePrincipalId: viper.GetString("service-principal-id"),
16-
servicePrincipalSecret: viper.GetString("service-principal-secret"),
17-
tenant: viper.GetString("service-principal-tenant"),
18-
executor: executor,
17+
servicePrincipalId: viper.GetString("service-principal-id"),
18+
servicePrincipalSecret: viper.GetString("service-principal-secret"),
19+
tenant: viper.GetString("service-principal-tenant"),
20+
userAssignedManagedIdentityClientId: viper.GetString("user-assigned-managed-identity-client-id"),
21+
useManagedIdentity: viper.GetBool("use-managed-identity"),
22+
executor: executor,
1923
}
2024
}
2125

2226
// NewWithParams creates a new Login instance with the ability to provide all parameters directly
23-
func NewWithParams(executor commands.Executor, servicePrincipalId string, servicePrincipalSecret string, tenant string) *Login {
27+
func NewWithParams(executor commands.Executor, servicePrincipalId string, servicePrincipalSecret string, tenant string, userAssignedManagedIdentityClientId string, useManagedIdentity bool) *Login {
2428
return &Login{
25-
servicePrincipalId: servicePrincipalId,
26-
servicePrincipalSecret: servicePrincipalSecret,
27-
tenant: tenant,
28-
executor: executor,
29+
servicePrincipalId: servicePrincipalId,
30+
servicePrincipalSecret: servicePrincipalSecret,
31+
tenant: tenant,
32+
userAssignedManagedIdentityClientId: userAssignedManagedIdentityClientId,
33+
useManagedIdentity: useManagedIdentity,
34+
executor: executor,
2935
}
3036
}

pkg/recipes/azure_login/login.go

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,44 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"os"
8+
"strings"
9+
710
"github.com/conplementag/cops-hq/v2/internal"
811
"github.com/conplementag/cops-hq/v2/pkg/commands"
912
"github.com/conplementag/cops-hq/v2/pkg/error_handling"
1013
"github.com/sirupsen/logrus"
11-
"os"
12-
"strings"
1314
)
1415

1516
type Login struct {
16-
servicePrincipalId string
17-
servicePrincipalSecret string
18-
tenant string
19-
executor commands.Executor
17+
servicePrincipalId string
18+
servicePrincipalSecret string
19+
userAssignedManagedIdentityClientId string
20+
useManagedIdentity bool
21+
tenant string
22+
executor commands.Executor
2023
}
2124

2225
// Login logs the currently configured user in AzureCLI and Terraform. If configured with service principal, it will
2326
// attempt a non-interactive login, otherwise a normal user login will be started.
2427
func (l *Login) Login() error {
25-
if l.useServicePrincipalLogin() {
28+
if l.useUserAssignedManagedIdentityLogin() {
29+
if l.tenant == "" {
30+
return errors.New("tenant must be given, when using user assigned managed identity")
31+
}
32+
33+
logrus.Info("Login as user assigned managed identity: " + l.userAssignedManagedIdentityClientId)
34+
err := l.userAssignedManagedIdentityLogin(l.userAssignedManagedIdentityClientId, l.tenant)
35+
return internal.ReturnErrorOrPanic(err)
36+
} else if l.useSystemAssignedManagedIdentityLogin() {
37+
if l.tenant == "" {
38+
return errors.New("tenant must be given, when using system assigned managed identity")
39+
}
40+
41+
logrus.Info("Login as system assigned managed identity")
42+
err := l.systemAssignedManagedIdentityLogin(l.tenant)
43+
return internal.ReturnErrorOrPanic(err)
44+
} else if l.useServicePrincipalLogin() {
2645
if l.servicePrincipalSecret == "" {
2746
return internal.ReturnErrorOrPanic(errors.New("service principal secret must be given, when using service principal credentials"))
2847
}
@@ -68,6 +87,14 @@ func (l *Login) SetSubscription(subscriptionId string) error {
6887
return nil
6988
}
7089

90+
func (l *Login) useSystemAssignedManagedIdentityLogin() bool {
91+
return l.useManagedIdentity && l.userAssignedManagedIdentityClientId == ""
92+
}
93+
94+
func (l *Login) useUserAssignedManagedIdentityLogin() bool {
95+
return l.useManagedIdentity && l.userAssignedManagedIdentityClientId != ""
96+
}
97+
7198
func (l *Login) useServicePrincipalLogin() bool {
7299
return l.servicePrincipalId != ""
73100
}
@@ -96,6 +123,43 @@ func (l *Login) servicePrincipalLogin(servicePrincipal string, secret string, te
96123
return nil
97124
}
98125

126+
func (l *Login) userAssignedManagedIdentityLogin(userAssignedManagedIdentityClientId string, tenant string) error {
127+
// First, we log into the Azure CLI
128+
// see https://learn.microsoft.com/en-us/cli/azure/reference-index?view=azure-cli-latest#az-login hints for secrets starting with "-"
129+
commandText := "az login --identity --username " + userAssignedManagedIdentityClientId
130+
_, err := l.executor.Execute(commandText)
131+
132+
// Then, we also need to set the env variables required for Terraform if working with user assigned managed identities
133+
err1 := os.Setenv("ARM_CLIENT_ID", userAssignedManagedIdentityClientId)
134+
err2 := os.Setenv("ARM_USE_MSI", "true")
135+
err3 := os.Setenv("ARM_TENANT_ID", tenant)
136+
137+
if err != nil || err1 != nil || err2 != nil || err3 != nil {
138+
return internal.ReturnErrorOrPanic(fmt.Errorf("errors while logging in via user assigned managed identity: %v %v %v %v",
139+
err, err1, err2, err3))
140+
}
141+
142+
return nil
143+
}
144+
145+
func (l *Login) systemAssignedManagedIdentityLogin(tenant string) error {
146+
// First, we log into the Azure CLI
147+
// see https://learn.microsoft.com/en-us/cli/azure/reference-index?view=azure-cli-latest#az-login hints for secrets starting with "-"
148+
commandText := "az login --identity"
149+
_, err := l.executor.Execute(commandText)
150+
151+
// Then, we also need to set the env variables required for Terraform if working with system assigned managed identities
152+
err1 := os.Setenv("ARM_USE_MSI", "true")
153+
err2 := os.Setenv("ARM_TENANT_ID", tenant)
154+
155+
if err != nil || err1 != nil || err2 != nil {
156+
return internal.ReturnErrorOrPanic(fmt.Errorf("errors while logging in via system assigned managed identity: %v %v %v",
157+
err, err1, err2))
158+
}
159+
160+
return nil
161+
}
162+
99163
func (l *Login) isUserAlreadyLoggedIn() (bool, error) {
100164
// since we actually rely on errors to test if user is logged in, we will shortly suppress the executor panics
101165
previousPanicSetting := error_handling.PanicOnAnyError

pkg/recipes/azure_login/login_test.go

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ package azure_login
33
import (
44
"encoding/json"
55
"errors"
6-
"github.com/conplementag/cops-hq/v2/pkg/commands"
7-
"github.com/stretchr/testify/mock"
86
"strings"
97
"testing"
8+
9+
"github.com/conplementag/cops-hq/v2/pkg/commands"
10+
"github.com/stretchr/testify/mock"
1011
)
1112

1213
func Test_TriggersServicePrincipalLogin_WhenIdProvided(t *testing.T) {
1314
// Arrange
1415
executor := &loginExecutorMock{}
15-
azureLogin := NewWithParams(executor, "abcd", "secret", "tenantId")
16+
azureLogin := NewWithParams(executor, "abcd", "secret", "tenantId", "", false)
1617

1718
executor.On("ExecuteSilent", mock.MatchedBy(func(command string) bool {
1819
return strings.Contains(command, "--service-principal") && strings.Contains(command, "abcd")
@@ -25,6 +26,54 @@ func Test_TriggersServicePrincipalLogin_WhenIdProvided(t *testing.T) {
2526
executor.AssertExpectations(t)
2627
}
2728

29+
func Test_TriggersUserAssignedManagedIdentityLogin_WhenClientIdAndFlagProvided(t *testing.T) {
30+
// Arrange
31+
executor := &loginExecutorMock{}
32+
azureLogin := NewWithParams(executor, "abcd", "secret", "tenantId", "umi-clientid", true)
33+
34+
executor.On("Execute", mock.MatchedBy(func(command string) bool {
35+
return command == "az login --identity --username umi-clientid"
36+
}))
37+
38+
// Act
39+
azureLogin.Login()
40+
41+
// Assert
42+
executor.AssertExpectations(t)
43+
}
44+
45+
func Test_TriggersSystemAssignedManagedIdentityLogin_WhenOnlyFlagProvided(t *testing.T) {
46+
// Arrange
47+
executor := &loginExecutorMock{}
48+
azureLogin := NewWithParams(executor, "abcd", "secret", "tenantId", "", true)
49+
50+
executor.On("Execute", mock.MatchedBy(func(command string) bool {
51+
return command == "az login --identity"
52+
}))
53+
54+
// Act
55+
azureLogin.Login()
56+
57+
// Assert
58+
executor.AssertExpectations(t)
59+
}
60+
61+
func Test_TriggersServicePrincipalLogin_WhenIdProvidedAndUamIdProvidedButMiFlagNotProvided(t *testing.T) {
62+
// Arrange
63+
executor := &loginExecutorMock{}
64+
azureLogin := NewWithParams(executor, "abcd", "secret", "tenantId", "umi-clientid", false)
65+
66+
executor.On("ExecuteSilent", mock.MatchedBy(func(command string) bool {
67+
return strings.Contains(command, "--service-principal") && strings.Contains(command, "abcd") && !strings.Contains(command, "--identity")
68+
}))
69+
70+
// Act
71+
azureLogin.Login()
72+
73+
// Assert
74+
executor.AssertExpectations(t)
75+
}
76+
2877
func Test_TriggersNoLogin_WhenUserAlreadyLoggedIn(t *testing.T) {
2978
// Arrange
3079
executor := &loginExecutorMock{}
@@ -90,6 +139,8 @@ func (e *loginExecutorMock) Execute(command string) (string, error) {
90139
} else {
91140
return "not logged in", errors.New("not logged int")
92141
}
142+
} else if strings.Contains(command, "--identity") {
143+
return "logged in with managed identity", nil
93144
}
94145

95146
return "unknown command for the Execute mock called, but let's return successfully anyways", nil

0 commit comments

Comments
 (0)