authorizer.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. package aws
  2. import (
  3. "context"
  4. "fmt"
  5. "github.com/aws/aws-sdk-go-v2/aws"
  6. awsconfig "github.com/aws/aws-sdk-go-v2/config"
  7. "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
  8. "github.com/aws/aws-sdk-go-v2/service/sts"
  9. "github.com/opencost/opencost/core/pkg/util/json"
  10. "github.com/opencost/opencost/pkg/cloud"
  11. "golang.org/x/oauth2/google"
  12. "google.golang.org/api/idtoken"
  13. "google.golang.org/api/option"
  14. )
  15. const AccessKeyAuthorizerType = "AWSAccessKey"
  16. const ServiceAccountAuthorizerType = "AWSServiceAccount"
  17. const AssumeRoleAuthorizerType = "AWSAssumeRole"
  18. const GoogleWebIdentityAuthorizerType = "GoogleWebIdentity"
  19. // Authorizer implementations provide aws.Config for AWS SDK calls
  20. type Authorizer interface {
  21. cloud.Authorizer
  22. CreateAWSConfig(string) (aws.Config, error)
  23. }
  24. // SelectAuthorizerByType is an implementation of AuthorizerSelectorFn and acts as a register for Authorizer types
  25. func SelectAuthorizerByType(typeStr string) (Authorizer, error) {
  26. switch typeStr {
  27. case AccessKeyAuthorizerType:
  28. return &AccessKey{}, nil
  29. case ServiceAccountAuthorizerType:
  30. return &ServiceAccount{}, nil
  31. case AssumeRoleAuthorizerType:
  32. return &AssumeRole{}, nil
  33. default:
  34. return nil, fmt.Errorf("AWS: provider authorizer type '%s' is not valid", typeStr)
  35. }
  36. }
  37. // AccessKey holds AWS credentials and fulfils the awsV2.CredentialsProvider interface
  38. type AccessKey struct {
  39. ID string `json:"id"`
  40. Secret string `json:"secret"`
  41. }
  42. // MarshalJSON custom json marshalling functions, sets properties as tagged in struct and sets the authorizer type property
  43. func (ak *AccessKey) MarshalJSON() ([]byte, error) {
  44. fmap := make(map[string]any, 3)
  45. fmap[cloud.AuthorizerTypeProperty] = AccessKeyAuthorizerType
  46. fmap["id"] = ak.ID
  47. fmap["secret"] = ak.Secret
  48. return json.Marshal(fmap)
  49. }
  50. // Retrieve returns a set of awsV2 credentials using the AccessKey's key and secret.
  51. // This fulfils the awsV2.CredentialsProvider interface contract.
  52. func (ak *AccessKey) Retrieve(ctx context.Context) (aws.Credentials, error) {
  53. return aws.Credentials{
  54. AccessKeyID: ak.ID,
  55. SecretAccessKey: ak.Secret,
  56. }, nil
  57. }
  58. func (ak *AccessKey) Validate() error {
  59. if ak.ID == "" {
  60. return fmt.Errorf("AccessKey: missing ID")
  61. }
  62. if ak.Secret == "" {
  63. return fmt.Errorf("AccessKey: missing Secret")
  64. }
  65. return nil
  66. }
  67. func (ak *AccessKey) Equals(config cloud.Config) bool {
  68. if config == nil {
  69. return false
  70. }
  71. thatConfig, ok := config.(*AccessKey)
  72. if !ok {
  73. return false
  74. }
  75. if ak.ID != thatConfig.ID {
  76. return false
  77. }
  78. if ak.Secret != thatConfig.Secret {
  79. return false
  80. }
  81. return true
  82. }
  83. func (ak *AccessKey) Sanitize() cloud.Config {
  84. return &AccessKey{
  85. ID: ak.ID,
  86. Secret: cloud.Redacted,
  87. }
  88. }
  89. // CreateAWSConfig creates an AWS SDK V2 Config for the credentials that it contains for the provided region
  90. func (ak *AccessKey) CreateAWSConfig(region string) (cfg aws.Config, err error) {
  91. err = ak.Validate()
  92. if err != nil {
  93. return cfg, err
  94. }
  95. // The AWS SDK v2 requires an object fulfilling the CredentialsProvider interface, which cloud.AccessKey does
  96. cfg, err = awsconfig.LoadDefaultConfig(context.TODO(), awsconfig.WithCredentialsProvider(ak), awsconfig.WithRegion(region))
  97. if err != nil {
  98. return cfg, fmt.Errorf("failed to initialize AWS SDK config for region %s: %s", region, err)
  99. }
  100. return cfg, nil
  101. }
  102. // ServiceAccount uses pod annotations along with a service account to authenticate integrations
  103. type ServiceAccount struct{}
  104. // MarshalJSON custom json marshalling functions, sets properties as tagged in struct and sets the authorizer type property
  105. func (sa *ServiceAccount) MarshalJSON() ([]byte, error) {
  106. fmap := make(map[string]any, 1)
  107. fmap[cloud.AuthorizerTypeProperty] = ServiceAccountAuthorizerType
  108. return json.Marshal(fmap)
  109. }
  110. // Check has nothing to check at this level, connection will fail if Pod Annotation and Service Account are not configured correctly
  111. func (sa *ServiceAccount) Validate() error {
  112. return nil
  113. }
  114. func (sa *ServiceAccount) Equals(config cloud.Config) bool {
  115. if config == nil {
  116. return false
  117. }
  118. _, ok := config.(*ServiceAccount)
  119. return ok
  120. }
  121. func (sa *ServiceAccount) Sanitize() cloud.Config {
  122. return &ServiceAccount{}
  123. }
  124. func (sa *ServiceAccount) CreateAWSConfig(region string) (aws.Config, error) {
  125. cfg, err := awsconfig.LoadDefaultConfig(context.TODO(), awsconfig.WithRegion(region))
  126. if err != nil {
  127. return cfg, fmt.Errorf("failed to initialize AWS SDK config for region from annotation %s: %s", region, err)
  128. }
  129. return cfg, nil
  130. }
  131. // AssumeRole is a wrapper for another Authorizer which adds an assumed role to the configuration
  132. type AssumeRole struct {
  133. Authorizer Authorizer `json:"authorizer"`
  134. RoleARN string `json:"roleARN"`
  135. }
  136. // MarshalJSON custom json marshalling functions, sets properties as tagged in struct and sets the authorizer type property
  137. func (ara *AssumeRole) MarshalJSON() ([]byte, error) {
  138. fmap := make(map[string]any, 3)
  139. fmap[cloud.AuthorizerTypeProperty] = AssumeRoleAuthorizerType
  140. fmap["roleARN"] = ara.RoleARN
  141. fmap["authorizer"] = ara.Authorizer
  142. return json.Marshal(fmap)
  143. }
  144. // UnmarshalJSON is required for AssumeRole because it needs to unmarshal an Authorizer interface
  145. func (ara *AssumeRole) UnmarshalJSON(b []byte) error {
  146. var f interface{}
  147. err := json.Unmarshal(b, &f)
  148. if err != nil {
  149. return err
  150. }
  151. fmap := f.(map[string]interface{})
  152. roleARN, err := cloud.GetInterfaceValue[string](fmap, "roleARN")
  153. if err != nil {
  154. return fmt.Errorf("StorageConfiguration: UnmarshalJSON: %s", err.Error())
  155. }
  156. ara.RoleARN = roleARN
  157. authAny, ok := fmap["authorizer"]
  158. if !ok {
  159. return fmt.Errorf("AssumeRole: UnmarshalJSON: missing Authorizer")
  160. }
  161. authorizer, err := cloud.AuthorizerFromInterface(authAny, SelectAuthorizerByType)
  162. if err != nil {
  163. return fmt.Errorf("AssumeRole: UnmarshalJSON: %s", err.Error())
  164. }
  165. ara.Authorizer = authorizer
  166. return nil
  167. }
  168. func (ara *AssumeRole) CreateAWSConfig(region string) (aws.Config, error) {
  169. cfg, _ := ara.Authorizer.CreateAWSConfig(region)
  170. // Create the credentials from AssumeRoleProvider to assume the role
  171. // referenced by the RoleARN.
  172. stsSvc := sts.NewFromConfig(cfg)
  173. creds := stscreds.NewAssumeRoleProvider(stsSvc, ara.RoleARN)
  174. cfg.Credentials = aws.NewCredentialsCache(creds)
  175. return cfg, nil
  176. }
  177. func (ara *AssumeRole) Validate() error {
  178. if ara.Authorizer == nil {
  179. return fmt.Errorf("AssumeRole: missing base Authorizer")
  180. }
  181. err := ara.Authorizer.Validate()
  182. if err != nil {
  183. return err
  184. }
  185. if ara.RoleARN == "" {
  186. return fmt.Errorf("AssumeRole: missing RoleARN configuration")
  187. }
  188. return nil
  189. }
  190. func (ara *AssumeRole) Equals(config cloud.Config) bool {
  191. if config == nil {
  192. return false
  193. }
  194. thatConfig, ok := config.(*AssumeRole)
  195. if !ok {
  196. return false
  197. }
  198. if ara.Authorizer != nil {
  199. if !ara.Authorizer.Equals(thatConfig.Authorizer) {
  200. return false
  201. }
  202. } else {
  203. if thatConfig.Authorizer != nil {
  204. return false
  205. }
  206. }
  207. if ara.RoleARN != thatConfig.RoleARN {
  208. return false
  209. }
  210. return true
  211. }
  212. func (ara *AssumeRole) Sanitize() cloud.Config {
  213. return &AssumeRole{
  214. Authorizer: ara.Authorizer.Sanitize().(Authorizer),
  215. RoleARN: ara.RoleARN,
  216. }
  217. }
  218. type GoogleWebIdentity struct {
  219. RoleARN string `json:"roleARN"`
  220. TokenRetriever GoogleIDTokenRetriever `json:"tokenRetriever"`
  221. }
  222. type GoogleIDTokenRetriever struct {
  223. Aud string `json:"aud"`
  224. }
  225. func (gitr *GoogleIDTokenRetriever) GetIdentityToken() ([]byte, error) {
  226. ctx := context.Background()
  227. res := []byte{}
  228. credentials, err := google.FindDefaultCredentials(ctx)
  229. if err != nil {
  230. return res, fmt.Errorf("failed to find default credentials: %v", err)
  231. }
  232. ts, err := idtoken.NewTokenSource(ctx, gitr.Aud, option.WithCredentials(credentials))
  233. if err != nil {
  234. return res, fmt.Errorf("failed to create ID token source: %w", err)
  235. }
  236. t, err := ts.Token()
  237. if err != nil {
  238. return res, fmt.Errorf("failed to receive ID token from metadata server: %w", err)
  239. }
  240. return []byte(t.AccessToken), nil
  241. }
  242. func (wea *GoogleWebIdentity) CreateAWSConfig(region string) (aws.Config, error) {
  243. cfg, err := awsconfig.LoadDefaultConfig(context.TODO(), awsconfig.WithRegion(region))
  244. if err != nil {
  245. return cfg, fmt.Errorf("failed to initialize AWS SDK config for region from annotation %s: %s", region, err)
  246. }
  247. stsSvc := sts.NewFromConfig(cfg)
  248. creds := stscreds.NewWebIdentityRoleProvider(stsSvc, wea.RoleARN, &wea.TokenRetriever)
  249. cfg.Credentials = aws.NewCredentialsCache(creds)
  250. return cfg, nil
  251. }
  252. func (wea *GoogleWebIdentity) MarshalJSON() ([]byte, error) {
  253. fmap := make(map[string]any, 1)
  254. fmap[cloud.AuthorizerTypeProperty] = GoogleWebIdentityAuthorizerType
  255. fmap["roleARN"] = wea.RoleARN
  256. fmap["tokenRetriever"] = wea.TokenRetriever
  257. return json.Marshal(fmap)
  258. }
  259. func (wea *GoogleWebIdentity) Validate() error {
  260. if wea.TokenRetriever.Aud == "" {
  261. return fmt.Errorf("GoogleWebIdenity: missing token retriver audience")
  262. }
  263. if wea.RoleARN == "" {
  264. return fmt.Errorf("GoogleWebIdenity: missing RoleARN configuration")
  265. }
  266. return nil
  267. }
  268. func (wea *GoogleWebIdentity) Equals(config cloud.Config) bool {
  269. if config == nil {
  270. return false
  271. }
  272. thatConfig, ok := config.(*GoogleWebIdentity)
  273. if !ok {
  274. return false
  275. }
  276. return wea.RoleARN == thatConfig.RoleARN && wea.TokenRetriever.Aud == thatConfig.TokenRetriever.Aud
  277. }
  278. func (wea *GoogleWebIdentity) Sanitize() cloud.Config {
  279. return &GoogleWebIdentity{
  280. RoleARN: wea.RoleARN,
  281. TokenRetriever: GoogleIDTokenRetriever{
  282. Aud: wea.TokenRetriever.Aud,
  283. },
  284. }
  285. }