-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
api.go
193 lines (167 loc) · 5.64 KB
/
api.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package iamoidc
import (
"context"
"crypto/sha1"
"crypto/tls"
"fmt"
"net/http"
"net/url"
"github.com/aws/aws-sdk-go-v2/service/iam"
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/weaveworks/eksctl/pkg/awsapi"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/pkg/errors"
cft "github.com/weaveworks/eksctl/pkg/cfn/template"
)
const defaultAudience = "sts.amazonaws.com"
// OpenIDConnectManager hold information about IAM OIDC integration
type OpenIDConnectManager struct {
accountID string
partition string
audience string
tags map[string]string
issuerURL *url.URL
insecureSkipVerify bool
issuerCAThumbprint string
ProviderARN string
iam awsapi.IAM
}
// UnsupportedOIDCError represents an unsupported OIDC error
type UnsupportedOIDCError struct {
Message string
}
func (u *UnsupportedOIDCError) Error() string {
return u.Message
}
// NewOpenIDConnectManager constructs a new IAM OIDC manager instance.
// It returns an error if the issuer URL is invalid
func NewOpenIDConnectManager(iamapi awsapi.IAM, accountID, issuer, partition string, tags map[string]string) (*OpenIDConnectManager, error) {
issuerURL, err := url.Parse(issuer)
if err != nil {
return nil, errors.Wrapf(err, "parsing OIDC issuer URL")
}
if issuerURL.Scheme != "https" {
return nil, fmt.Errorf("unsupported URL scheme %q", issuerURL.Scheme)
}
if issuerURL.Port() == "" {
issuerURL.Host += ":443"
}
m := &OpenIDConnectManager{
iam: iamapi,
accountID: accountID,
partition: partition,
tags: tags,
audience: defaultAudience,
issuerURL: issuerURL,
}
return m, nil
}
// CheckProviderExists will return true when the provider exists, it may return errors
// if it was unable to call IAM API
func (m *OpenIDConnectManager) CheckProviderExists(ctx context.Context) (bool, error) {
input := &iam.GetOpenIDConnectProviderInput{
OpenIDConnectProviderArn: aws.String(
fmt.Sprintf("arn:%s:iam::%s:oidc-provider/%s", m.partition, m.accountID, m.hostnameAndPath()),
),
}
_, err := m.iam.GetOpenIDConnectProvider(ctx, input)
if err != nil {
var oe *iamtypes.NoSuchEntityException
if errors.As(err, &oe) {
return false, nil
}
return false, err
}
m.ProviderARN = *input.OpenIDConnectProviderArn
return true, nil
}
// CreateProvider will retrieve CA root certificate and compute its thumbprint for the
// by connecting to it and create the provider using IAM API
func (m *OpenIDConnectManager) CreateProvider(ctx context.Context) error {
if err := m.getIssuerCAThumbprint(); err != nil {
return err
}
var tags []iamtypes.Tag
for k, v := range m.tags {
tags = append(tags, iamtypes.Tag{
Key: aws.String(k),
Value: aws.String(v),
})
}
input := &iam.CreateOpenIDConnectProviderInput{
ClientIDList: []string{m.audience},
ThumbprintList: []string{m.issuerCAThumbprint},
// It has no name or tags, it's keyed to the URL
Url: aws.String(m.issuerURL.String()),
Tags: tags,
}
output, err := m.iam.CreateOpenIDConnectProvider(ctx, input)
if err != nil {
return errors.Wrap(err, "creating OIDC provider")
}
m.ProviderARN = *output.OpenIDConnectProviderArn
return nil
}
// DeleteProvider will delete the provider using IAM API, it may return an error
// the API call fails
func (m *OpenIDConnectManager) DeleteProvider(ctx context.Context) error {
// TODO: the ARN is deterministic, but we need to consider tracking
// it somehow; it's possible to get a dangling resource if cluster
// deletion was done by a version of eksctl that is not OIDC-aware,
// as we don't use CloudFormation;
// finding dangling resource will require looking at all clusters...
input := &iam.DeleteOpenIDConnectProviderInput{
OpenIDConnectProviderArn: &m.ProviderARN,
}
if _, err := m.iam.DeleteOpenIDConnectProvider(ctx, input); err != nil {
return errors.Wrap(err, "deleting OIDC provider")
}
return nil
}
// getIssuerCAThumbprint obtains thumbprint of root CA by connecting to the
// OIDC issuer and parsing certificates
func (m *OpenIDConnectManager) getIssuerCAThumbprint() error {
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: m.insecureSkipVerify,
MinVersion: tls.VersionTLS12,
},
Proxy: http.ProxyFromEnvironment,
},
}
response, err := client.Get(m.issuerURL.String())
if err != nil {
return errors.Wrap(err, "connecting to issuer OIDC")
}
defer response.Body.Close()
if response.TLS != nil {
if numCerts := len(response.TLS.PeerCertificates); numCerts >= 1 {
root := response.TLS.PeerCertificates[numCerts-1]
m.issuerCAThumbprint = fmt.Sprintf("%x", sha1.Sum(root.Raw))
return nil
}
}
return fmt.Errorf("unable to get OIDC issuer's certificate")
}
// MakeAssumeRolePolicyDocumentWithServiceAccountConditions constructs a trust policy document for the given
// provider
func (m *OpenIDConnectManager) MakeAssumeRolePolicyDocumentWithServiceAccountConditions(serviceAccountNamespace, serviceAccountName string) cft.MapOfInterfaces {
subject := fmt.Sprintf("system:serviceaccount:%s:%s", serviceAccountNamespace, serviceAccountName)
return cft.MakeAssumeRoleWithWebIdentityPolicyDocument(m.ProviderARN, cft.MapOfInterfaces{
"StringEquals": map[string]string{
m.hostnameAndPath() + ":sub": subject,
m.hostnameAndPath() + ":aud": m.audience,
},
})
}
func (m *OpenIDConnectManager) MakeAssumeRolePolicyDocument() cft.MapOfInterfaces {
return cft.MakeAssumeRoleWithWebIdentityPolicyDocument(m.ProviderARN, cft.MapOfInterfaces{
"StringEquals": map[string]string{
m.hostnameAndPath() + ":aud": m.audience,
},
})
}
func (m *OpenIDConnectManager) hostnameAndPath() string {
return m.issuerURL.Hostname() + m.issuerURL.Path
}