authorizer.go 9.8 KB

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