Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add avn provider #195

Merged
merged 5 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
# Cloud-Key-Client
[![CircleCI](https://circleci.com/gh/ovotech/cloud-key-client.svg?style=svg&circle-token=4a7b48b664bf017b6256234f5de24c5b70c54168)](https://circleci.com/gh/ovotech/cloud-key-client)

[![CircleCI](https://circleci.com/gh/ovotech/cloud-key-client.svg?style=svg&circle-token=4a7b48b664bf017b6256234f5de24c5b70c54168)](https://circleci.com/gh/ovotech/cloud-key-client)

Cloud-Key-Client is a Golang client that connects up to cloud providers either
to collect details of Service Account keys, or manipulate them.


## Install as a Go Dependency

```go
go get -u github.com/ovotech/cloud-key-client
```


## Getting Started

```go
Expand All @@ -36,10 +35,16 @@ func main() {
// no need to specify any account ID here
Provider: "aws",
}
// create an Aiven provider
aivenProvider := keys.Provider{
Provider: "aiven",
Token: "my-aiven-api-token"
}

// add both providers to the slice
providers = append(providers, gcpProvider)
providers = append(providers, awsProvider)
providers = append(providers, aivenProvider)

// use the cloud-key-client
keys, err := keys.Keys(providers, true)
Expand All @@ -59,17 +64,17 @@ func main() {

## Purpose

This client could be useful for obtaining key metadata, such as age, and
performing create and delete operations for key rotation. Multiple providers
This client could be useful for obtaining key metadata, such as age, and
performing create and delete operations for key rotation. Multiple providers
can be accessed through a single interface.


## Integrations

The following cloud providers have been integrated:

* AWS
* GCP
- AWS
- Aiven
- GCP

No config is required, you simply need to pass a slice of `Provider` structs to
the `keys()` func.
Expand Down
228 changes: 228 additions & 0 deletions aiven.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package keys

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
)

const aivenTokenEndpoint string = "https://api.aiven.io/v1/access_token"
const fullAccountSeparator string = ":"

// AivenKey type
type AivenKey struct{}

// Error type
type Error struct {
Message string `json:"message"`
MoreInfo string `json:"more_info"`
Status int `json:"status"`
}

// Token type
type Token struct {
CreateTime string `json:"create_time"`
CurrentlyActive bool `json:"currently_active"`
Description string `json:"description"`
TokenPrefix string `json:"token_prefix"`
}

// ListTokensResponse type
type ListTokensResponse struct {
Errors []Error `json:"errors"`
Message string `json:"message"`
Tokens []Token `json:"tokens"`
}

// CreateTokenResponse type
type CreateTokenResponse struct {
CreateTime string `json:"create_time"`
CreatedManually bool `json:"created_manually"`
Errors []Error `json:"errors"`
ExtendWhenUsed bool `json:"extend_when_used"`
FullToken string `json:"full_token"`
MaxAgeSeconds int `json:"max_age_seconds"`
Message string `json:"message"`
TokenPrefix string `json:"token_prefix"`
}

// RevokeTokenResponse type
type RevokeTokenResponse struct {
Errors []Error `json:"errors"`
Message string `json:"message"`
}

// Generic functions for sending an HTTP request
func doGenericHTTPReq(method, url, token string, payload io.Reader) (body []byte, err error) {
client := http.Client{}
req, err := http.NewRequest(method, url, payload)
if err != nil {
return
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}

// Get the listTokensResponse from the Aiven API
func listTokensResponse(token string) (ltr ListTokensResponse, err error) {
// https://api.aiven.io/doc/#tag/User/operation/AccessTokenList
body, err := doGenericHTTPReq(
http.MethodGet,
aivenTokenEndpoint,
token,
nil,
)
if err != nil {
return
}
err = json.Unmarshal(body, &ltr)
return
}

// Get the createTokenResponse from the Aiven API
func createTokenResponse(token, description string) (ctr CreateTokenResponse, err error) {
// https://api.aiven.io/doc/#tag/User/operation/AccessTokenCreate
jsonStr := []byte(fmt.Sprintf("{\"description\":\"%s\"}", description))
body, err := doGenericHTTPReq(
http.MethodPost,
aivenTokenEndpoint,
token,
bytes.NewBuffer(jsonStr),
)
if err != nil {
return
}
err = json.Unmarshal(body, &ctr)
return
}

// Get the revokeTokenResponse from the Aiven API
func revokeTokenResponse(tokenPrefix, token string) (rtr RevokeTokenResponse, err error) {
// https://api.aiven.io/doc/#tag/User/operation/AccessTokenRevoke
body, err := doGenericHTTPReq(
http.MethodDelete,
fmt.Sprintf("%s/%s", aivenTokenEndpoint, tokenPrefix),
token,
nil,
)
if err != nil {
return
}
err = json.Unmarshal(body, &rtr)
if err != nil {
err = fmt.Errorf("Failed unmarshalling response: %s, response from Aiven API: %s", err, string(body[:]))
}
return
}

// Transform a slice of errors (returned in Aiven response) to a single error
func handleAPIErrors(errs []Error) (err error) {
var errorMsgs []string
for _, error := range errs {
msg := fmt.Sprintf("msg: %s, status: %d", error.Message, error.Status)
errorMsgs = append(errorMsgs, msg)
}
return errors.New(strings.Join(errorMsgs, ","))
}

// Return a status string (active|inactive)
func status(currentlyActive bool) string {
status := "Inactive"
if currentlyActive {
status = "Active"
}
return status
}

// Get the description of a key/token from a 'fullAccount' identifier
func tokenPrefixDescriptionFromFullAccount(account string) (tokenPrefix, tokenDescription string, err error) {
tokenPrefix, tokenDescription, found := strings.Cut(account, fullAccountSeparator)
if !found {
err = fmt.Errorf("Separator %s not found in fullAccount: %s", fullAccountSeparator, account)
return
}
return
}

// Keys returns a slice of keys (or tokens in this case) for the user who
// owns the apiToken
func (a AivenKey) Keys(project string, includeInactiveKeys bool, apiToken string) (keys []Key, err error) {
ltr, err := listTokensResponse(apiToken)
if err != nil {
return
}
if len(ltr.Errors) > 0 {
err = handleAPIErrors(ltr.Errors)
return
}
for _, token := range ltr.Tokens {
var createTime time.Time
if createTime, err = time.Parse(aivenTimeFormat, token.CreateTime); err != nil {
return
}
// ignore the token if it has no description (this is the identifier
// we use to track tokens down that are configured for rotation)
if token.Description != "" {
key := Key{
FullAccount: fmt.Sprintf("%s%s%s", token.TokenPrefix, fullAccountSeparator, token.Description),
Age: time.Since(createTime).Minutes(),
ID: token.TokenPrefix,
Name: token.Description,
Provider: Provider{Provider: aivenProviderString, Token: apiToken},
Status: status(token.CurrentlyActive),
}
keys = append(keys, key)
}
}
return
}

// CreateKey creates a new Aiven API token
func (a AivenKey) CreateKey(project, account, token string) (keyID string, newKey string, err error) {
if account == "" {
err = errors.New("The account string is empty; this is required to explicitly define which keys/tokens to interact with")
return
}
_, description, err := tokenPrefixDescriptionFromFullAccount(account)
if err != nil {
return
}
ctr, err := createTokenResponse(token, description)
if err != nil {
return
}
if len(ctr.Errors) > 0 {
err = handleAPIErrors(ctr.Errors)
return
}
keyID = ctr.TokenPrefix
newKey = ctr.FullToken
return
}

// DeleteKey deletes the specified Aiven API token
func (a AivenKey) DeleteKey(project, account, keyID, token string) (err error) {
tokenPrefix, _, err := tokenPrefixDescriptionFromFullAccount(account)
if err != nil {
return
}
rtr, err := revokeTokenResponse(tokenPrefix, token)
if err != nil {
return
}
if len(rtr.Errors) > 0 {
err = handleAPIErrors(rtr.Errors)
}
return
}
8 changes: 4 additions & 4 deletions aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const (
)

//Keys returns a slice of keys from any authorised accounts
func (a AwsKey) Keys(project string, includeInactiveKeys bool) (keys []Key, err error) {
func (a AwsKey) Keys(project string, includeInactiveKeys bool, token string) (keys []Key, err error) {
var svc *awsiam.IAM
if svc, err = iamService(); err != nil {
return
Expand All @@ -49,7 +49,7 @@ func (a AwsKey) Keys(project string, includeInactiveKeys bool) (keys []Key, err
0,
strings.Join([]string{*awsKey.UserName,
keyID[len(keyID)-numIDValuesInName:]}, "_"),
Provider{awsProviderString, ""},
Provider{Provider: awsProviderString},
*awsKey.Status,
})
}
Expand All @@ -59,7 +59,7 @@ func (a AwsKey) Keys(project string, includeInactiveKeys bool) (keys []Key, err
}

//CreateKey creates a key in the provided account
func (a AwsKey) CreateKey(project, account string) (keyID, newKey string, err error) {
func (a AwsKey) CreateKey(project, account, token string) (keyID, newKey string, err error) {
var svc *awsiam.IAM
if svc, err = iamService(); err != nil {
return
Expand Down Expand Up @@ -87,7 +87,7 @@ func (a AwsKey) CreateKey(project, account string) (keyID, newKey string, err er
}

//DeleteKey deletes the specified key from the specified account
func (a AwsKey) DeleteKey(project, account, keyID string) (err error) {
func (a AwsKey) DeleteKey(project, account, keyID, token string) (err error) {
var svc *awsiam.IAM
if svc, err = iamService(); err != nil {
return
Expand Down
8 changes: 4 additions & 4 deletions gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type GcpKey struct{}
const gcpAccessKeyLimit = 10

//Keys returns a slice of keys from any authorised accounts
func (g GcpKey) Keys(project string, includeInactiveKeys bool) (keys []Key, err error) {
func (g GcpKey) Keys(project string, includeInactiveKeys bool, token string) (keys []Key, err error) {
if err = validateGcpProjectString(project); err != nil {
return
}
Expand Down Expand Up @@ -87,14 +87,14 @@ func keyFromGcpKey(gcpKey *gcpiam.ServiceAccountKey, project string) (key Key, e
math.Abs(time.Since(expiryTime).Minutes()),
strings.Join([]string{serviceAccountName,
keyID[len(keyID)-numIDValuesInName:]}, "_"),
Provider{gcpProviderString, project},
Provider{Provider: gcpProviderString, GcpProject: project},
"Active",
}
return
}

//CreateKey creates a key in the provided account
func (g GcpKey) CreateKey(project, account string) (keyID, newKey string, err error) {
func (g GcpKey) CreateKey(project, account, token string) (keyID, newKey string, err error) {
if err = validateGcpProjectString(project); err != nil {
return
}
Expand Down Expand Up @@ -126,7 +126,7 @@ func (g GcpKey) CreateKey(project, account string) (keyID, newKey string, err er
}

//DeleteKey deletes the specified key from the specified account
func (g GcpKey) DeleteKey(project, account, keyID string) (err error) {
func (g GcpKey) DeleteKey(project, account, keyID, token string) (err error) {
if err = validateGcpProjectString(project); err != nil {
return
}
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ require (
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/net v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 // indirect
google.golang.org/grpc v1.51.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
Loading