Skip to content
This repository has been archived by the owner on Feb 16, 2021. It is now read-only.

Commit

Permalink
CLOUDP:48406: Accept API keys through HTTP basic auth (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
fabianlindfors committed Aug 20, 2019
1 parent 199bb70 commit 75240b6
Show file tree
Hide file tree
Showing 18 changed files with 302 additions and 214 deletions.
22 changes: 2 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,14 @@ For instructions on how to install and use the MongoDB Atlas Service Broker plea

## Configuration

Configuration is handled with environment variables.

### Atlas API
Configuration is handled with environment variables. Logs are written to
`stderr` and each line is in a structured JSON format.

| Variable | Default | Description |
| -------- | ------- | ----------- |
| ATLAS_GROUP_ID | **Required** | Group in which to provision new clusters |
| ATLAS_PUBLIC_KEY | **Required** | Public part of the Atlas API key |
| ATLAS_PRIVATE_KEY | **Required** | Private part of the Atlas API key |
| ATLAS_BASE_URL | `https://cloud.mongodb.com` | Base URL used for Atlas API connections |

### Broker API Server

| Variable | Default | Description |
| -------- | ------- | ----------- |
| BROKER_USERNAME | **Required** | Username for basic auth against broker |
| BROKER_PASSWORD | **Required** | Password for basic auth against broker |
| BROKER_HOST | `127.0.0.1` | Address which the broker server listens on |
| BROKER_PORT | `4000` | Port which the broker server listens on |

### Logs

Logs are written to `stderr` and each line is in a structured JSON format.

| Variable | Default | Description |
| -------- | ------- | ----------- |
| BROKER_LOG_LEVEL | `INFO` | Accepted values: `DEBUG`, `INFO`, `WARN`, `ERROR` |

## License
Expand Down
18 changes: 8 additions & 10 deletions dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,21 @@ Follow these steps to test the broker in a Kubernetes cluster. For local testing
be found in the [Kubernetes docs](https://kubernetes.io/docs/tasks/service-catalog/install-service-catalog-using-helm/).
3. Build the Dockerfile and make the resulting image available in your cluster. If you are using
Minikube `dev/scripts/minikube-build.sh` can be used to build the image using Minikube's Docker
daemon. Update the deployment resource in `samples/kubernetes/deployment.yaml` to have `imagePullPolicy: Never`.
daemon. Update the deployment resource in `samples/kubernetes/deployment.yaml` to have
`imagePullPolicy: Never` and update the `ATLAS_BASE_URL` to whichever environment you're testing against.
4. Create a new namespace `atlas` by running `kubectl create namespace atlas`.
5. Create a secret called `atlas-api` containing the following keys:
- `base-url`for the Atlas API
- `group-id` for the project under which clusters should be deployed
- `public-key`for the API key
- `private-key`for the API key
5. Create a secret called `atlas-service-broker-auth` containing the following keys:
- `username` should be the Atlas group ID and public key combined as `<PUBLIC_KEY>@<GROUP_ID>`.
- `password` should be the Atlas private key.
6. Deploy the service broker by running `kubectl apply -f samples/kubernetes/deployment.yaml -n atlas`. This will create
a new deployment and a service of the image from step 2. It will also create a new secret containing the
basic auth credentials for the service catalog which are needed for the next step.
a new deployment and a service of the image from step 2.
7. Register the service broker with the service catalog by running `kubectl apply -f samples/kubernetes/service-broker.yaml -n atlas`.
8. Make sure the broker is ready by running `svcat get brokers`.
9. A new instance can be provisioned by running `kubectl create -f scripts/kubernetes/instance.yaml -n atlas`.
9. A new instance can be provisioned by running `kubectl create -f samples/kubernetes/instance.yaml -n atlas`.
The instance will be given the name `atlas-cluster-instance` and its status can be checked using `svcat get instances -n atlas`.
10. Once the instance is up and running, a binding can be created to gain access. A binding named
`atlas-cluster-binding` can be created by running `kubectl create -f
script/kubernetes/binding.yaml -n atlas`. The binding credentials will automatically be stored in a secret
samples/kubernetes/binding.yaml -n atlas`. The binding credentials will automatically be stored in a secret
of the same name.
11. After use, all bindings can be removed by running `svcat unbind atlas-cluser-instance -n atlas` and the
cluster can be deprovisioned using `svcat deprovision atlas-cluster-instance -n atlas`.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/go-stack/stack v1.8.0 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.3 // indirect
github.com/gorilla/mux v1.7.3
github.com/imdario/mergo v0.3.7 // indirect
github.com/kubernetes-incubator/service-catalog v0.2.1
github.com/kubernetes-sigs/service-catalog v0.2.1
Expand Down
39 changes: 13 additions & 26 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

"os"

atlasclient "github.com/mongodb/mongodb-atlas-service-broker/pkg/atlas"
"github.com/gorilla/mux"
atlasbroker "github.com/mongodb/mongodb-atlas-service-broker/pkg/broker"
"github.com/pivotal-cf/brokerapi"
)
Expand Down Expand Up @@ -82,39 +82,26 @@ func startBrokerServer() {
}
defer logger.Sync() // Flushes buffer, if any

// Try parsing Atlas client config.
baseURL := strings.TrimRight(getEnvOrDefault("ATLAS_BASE_URL", DefaultAtlasBaseURL), "/")
groupID := getEnvOrPanic("ATLAS_GROUP_ID")
publicKey := getEnvOrPanic("ATLAS_PUBLIC_KEY")
privateKey := getEnvOrPanic("ATLAS_PRIVATE_KEY")
broker := atlasbroker.NewBroker(logger)

client, err := atlasclient.NewClient(baseURL, groupID, publicKey, privateKey)
if err != nil {
logger.Fatal(err)
}
router := mux.NewRouter()
brokerapi.AttachRoutes(router, broker, NewLagerZapLogger(logger))

// Create broker with the previously created Atlas client.
broker := atlasbroker.NewBroker(client, logger)
// The auth middleware will convert basic auth credentials into an Atlas
// client.
baseURL := strings.TrimRight(getEnvOrDefault("ATLAS_BASE_URL", DefaultAtlasBaseURL), "/")
router.Use(atlasbroker.AuthMiddleware(baseURL))

// Mount broker server at the root.
http.Handle("/", router)

// Try parsing server config and set up broker API server.
username := getEnvOrPanic("BROKER_USERNAME")
password := getEnvOrPanic("BROKER_PASSWORD")
host := getEnvOrDefault("BROKER_HOST", DefaultServerHost)
port := getIntEnvOrDefault("BROKER_PORT", DefaultServerPort)

credentials := brokerapi.BrokerCredentials{
Username: username,
Password: password,
}

endpoint := host + ":" + strconv.Itoa(port)

// Mount broker server at the root.
http.Handle("/", brokerapi.New(broker, NewLagerZapLogger(logger), credentials))

logger.Infow("Starting API server", "releaseVersion", releaseVersion, "host", host, "port", port, "atlas_base_url", baseURL, "group_id", groupID)
logger.Infow("Starting API server", "releaseVersion", releaseVersion, "host", host, "port", port, "atlas_base_url", baseURL)

// Start broker HTTP server.
endpoint := host + ":" + strconv.Itoa(port)
if err = http.ListenAndServe(endpoint, nil); err != nil {
logger.Fatal(err)
}
Expand Down
35 changes: 21 additions & 14 deletions pkg/atlas/atlas.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@ type Client interface {
// HTTPClient is the main implementation of the Client interface which
// communicates with the Atlas API.
type HTTPClient struct {
baseURL string
groupID string
publicKey string
privateKey string
BaseURL string
GroupID string
PublicKey string
PrivateKey string

HTTP *http.Client
}

// Different errors the api may return.
var (
ErrUnauthorized = errors.New("Invalid API key")

ErrClusterNotFound = errors.New("Cluster not found")
ErrClusterAlreadyExists = errors.New("Cluster already exists")

Expand All @@ -51,26 +53,26 @@ const (
)

// NewClient will create a new HTTPClient with the specified connection details.
func NewClient(baseURL string, groupID string, publicKey string, privateKey string) (*HTTPClient, error) {
func NewClient(baseURL string, groupID string, publicKey string, privateKey string) *HTTPClient {
return &HTTPClient{
baseURL: baseURL,
groupID: groupID,
publicKey: publicKey,
privateKey: privateKey,
BaseURL: baseURL,
GroupID: groupID,
PublicKey: publicKey,
PrivateKey: privateKey,
HTTP: &http.Client{},
}, nil
}
}

// requestPublic will make a request to an endpoint in the public API.
// The URL will be constructed by prepending the group to the specified endpoint.
func (c *HTTPClient) requestPublic(method string, endpoint string, body interface{}, response interface{}) error {
url := fmt.Sprintf("%s%s/groups/%s/%s", c.baseURL, publicAPIPath, c.groupID, endpoint)
url := fmt.Sprintf("%s%s/groups/%s/%s", c.BaseURL, publicAPIPath, c.GroupID, endpoint)
return c.request(method, url, body, response)
}

// requestPrivate will make a request to an endpoint in the private API.
func (c *HTTPClient) requestPrivate(method string, endpoint string, body interface{}, response interface{}) error {
url := fmt.Sprintf("%s%s/%s", c.baseURL, privateAPIPath, endpoint)
url := fmt.Sprintf("%s%s/%s", c.BaseURL, privateAPIPath, endpoint)
return c.request(method, url, body, response)
}

Expand Down Expand Up @@ -127,6 +129,11 @@ func (c *HTTPClient) request(method string, url string, body interface{}, respon
return nil
}

// Invalid credentials will cause a 401 Unauthorized response.
if resp.StatusCode == http.StatusUnauthorized {
return ErrUnauthorized
}

// Decode error if request was unsuccessful.
var errorResponse struct {
Code string `json:"errorCode"`
Expand Down Expand Up @@ -164,8 +171,8 @@ func (c *HTTPClient) digestAuth(method string, endpoint string) (string, error)
parts["uri"] = endpointURL.RequestURI()

// User public and private key as username and password
parts["username"] = c.publicKey
parts["password"] = c.privateKey
parts["username"] = c.PublicKey
parts["password"] = c.PrivateKey

return getDigestAuthrization(parts), nil
}
Expand Down
4 changes: 1 addition & 3 deletions pkg/atlas/atlas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,9 @@ func setupTest(t *testing.T, expectedPath string, method string, status int, res
}
}))

atlas, err := NewClient(s.URL, groupID, publicKey, privateKey)
atlas := NewClient(s.URL, groupID, publicKey, privateKey)
atlas.HTTP = s.Client()

assert.NoError(t, err)

return atlas, s
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/atlas/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,5 @@ func (c *HTTPClient) GetCluster(name string) (*Cluster, error) {

// GetDashboardURL prepares the url where the specific cluster can be found in the Dashboard UI
func (c *HTTPClient) GetDashboardURL(clusterName string) string {
return fmt.Sprintf("%s/v2/%s#clusters/detail/%s", c.baseURL, c.groupID, clusterName)
return fmt.Sprintf("%s/v2/%s#clusters/detail/%s", c.BaseURL, c.GroupID, clusterName)
}
20 changes: 15 additions & 5 deletions pkg/broker/binding_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ type ConnectionDetails struct {
func (b Broker) Bind(ctx context.Context, instanceID string, bindingID string, details brokerapi.BindDetails, asyncAllowed bool) (spec brokerapi.Binding, err error) {
b.logger.Infow("Creating binding", "instance_id", instanceID, "binding_id", bindingID, "details", details)

client, err := atlasClientFromContext(ctx)
if err != nil {
return
}

// The service_id and plan_id are required to be valid per the specification, despite
// not being used for bindings. We look them up to ensure they can be found in the catalog.
provider, err := b.findProviderByServiceID(details.ServiceID)
provider, err := findProviderByServiceID(client, details.ServiceID)
if err != nil {
return
}
Expand All @@ -37,7 +42,7 @@ func (b Broker) Bind(ctx context.Context, instanceID string, bindingID string, d
}

// Fetch the cluster from Atlas to ensure it exists.
cluster, err := b.atlas.GetCluster(NormalizeClusterName(instanceID))
cluster, err := client.GetCluster(NormalizeClusterName(instanceID))
if err != nil {
b.logger.Errorw("Failed to get existing cluster", "error", err, "instance_id", instanceID)
err = atlasToAPIError(err)
Expand All @@ -60,7 +65,7 @@ func (b Broker) Bind(ctx context.Context, instanceID string, bindingID string, d
}

// Create a new Atlas database user from the generated definition.
_, err = b.atlas.CreateUser(*user)
_, err = client.CreateUser(*user)
if err != nil {
b.logger.Errorw("Failed to create Atlas database user", "error", err, "instance_id", instanceID, "binding_id", bindingID)
err = atlasToAPIError(err)
Expand All @@ -84,16 +89,21 @@ func (b Broker) Bind(ctx context.Context, instanceID string, bindingID string, d
func (b Broker) Unbind(ctx context.Context, instanceID string, bindingID string, details brokerapi.UnbindDetails, asyncAllowed bool) (spec brokerapi.UnbindSpec, err error) {
b.logger.Infow("Releasing binding", "instance_id", instanceID, "binding_id", bindingID, "details", details)

client, err := atlasClientFromContext(ctx)
if err != nil {
return
}

// Fetch the cluster from Atlas to ensure it exists.
_, err = b.atlas.GetCluster(NormalizeClusterName(instanceID))
_, err = client.GetCluster(NormalizeClusterName(instanceID))
if err != nil {
b.logger.Errorw("Failed to get existing cluster", "error", err, "instance_id", instanceID)
err = atlasToAPIError(err)
return
}

// Delete database user which has the binding ID as its username.
err = b.atlas.DeleteUser(bindingID)
err = client.DeleteUser(bindingID)
if err != nil {
b.logger.Errorw("Failed to delete Atlas database user", "error", err, "instance_id", instanceID, "binding_id", bindingID)
err = atlasToAPIError(err)
Expand Down
Loading

0 comments on commit 75240b6

Please sign in to comment.