gateway/manta: Add support for RBAC (#5332)

Manta has the ability to allow users to authenticate with a 
username other than the main account. We want to expose 
this functionality to minio manta gateway.
This commit is contained in:
Paul Stack 2018-01-05 10:00:29 +02:00 committed by Nitish Tiwari
parent b85c75996d
commit a1a98617ca
10 changed files with 180 additions and 68 deletions

View file

@ -32,7 +32,6 @@ import (
"github.com/joyent/triton-go/authentication"
tclient "github.com/joyent/triton-go/client"
"github.com/joyent/triton-go/storage"
"github.com/minio/cli"
minio "github.com/minio/minio/cmd"
"github.com/minio/minio/pkg/auth"
@ -68,6 +67,7 @@ ENVIRONMENT VARIABLES:
MINIO_ACCESS_KEY: The Manta account name.
MINIO_SECRET_KEY: A KeyID associated with the Manta account.
MANTA_KEY_MATERIAL: The path to the SSH Key associated with the Manta account if the MINIO_SECRET_KEY is not in SSH Agent.
MANTA_SUBUSER: The username of a user who has limited access to your account.
BROWSER:
MINIO_BROWSER: To disable web browser access, set this value to "off".
@ -140,7 +140,14 @@ func (g *Manta) NewGatewayLayer(creds auth.Credentials) (minio.GatewayLayer, err
keyMaterial := os.Getenv("MANTA_KEY_MATERIAL")
if keyMaterial == "" {
signer, err = authentication.NewSSHAgentSigner(creds.SecretKey, creds.AccessKey)
input := authentication.SSHAgentSignerInput{
KeyID: creds.SecretKey,
AccountName: creds.AccessKey,
}
if userName, ok := os.LookupEnv("MANTA_SUBUSER"); ok {
input.Username = userName
}
signer, err = authentication.NewSSHAgentSigner(input)
if err != nil {
return nil, errors.Trace(err)
}
@ -168,7 +175,16 @@ func (g *Manta) NewGatewayLayer(creds auth.Credentials) (minio.GatewayLayer, err
keyBytes = []byte(keyMaterial)
}
signer, err = authentication.NewPrivateKeySigner(creds.SecretKey, keyBytes, creds.AccessKey)
input := authentication.PrivateKeySignerInput{
KeyID: creds.SecretKey,
PrivateKeyMaterial: keyBytes,
AccountName: creds.AccessKey,
}
if userName, ok := os.LookupEnv("MANTA_SUBUSER"); ok {
input.Username = userName
}
signer, err = authentication.NewPrivateKeySigner(input)
if err != nil {
return nil, errors.Trace(err)
}

View file

@ -7,7 +7,8 @@ Minio Gateway adds Amazon S3 compatibility to Manta Object Storage.
docker run -p 9000:9000 --name manta-s3 \
-e "MINIO_ACCESS_KEY=joyentaccountname" \
-e "MINIO_SECRET_KEY=joyentkeyid" \
-e "MINIO_KEY_MATERIAL=~/.ssh/id_rsa
-e "MANTA_KEY_MATERIAL=~/.ssh/id_rsa" \
-e "MANTA_SUBUSER=devuser"
minio/minio gateway manta
```
@ -15,7 +16,8 @@ docker run -p 9000:9000 --name manta-s3 \
```
export MINIO_ACCESS_KEY=joyentaccountname
export MINIO_SECRET_KEY=joyentkeyid
export MINIO_KEY_MATERIAL=~/.ssh/id_rsa
export MANTA_KEY_MATERIAL=~/.ssh/id_rsa
export MANTA_SUBUSER=devuser
minio gateway manta
```
## Test using Minio Browser

View file

@ -1,5 +1,23 @@
## Unreleased
## 0.5.2 (December 28)
- Standardise the API SSH Signers input casing and naming
## 0.5.1 (December 28)
- Include leading '/' when working with SSH Agent signers
## 0.5.0 (December 28)
- Add support for RBAC in triton-go [#82]
This is a breaking change. No longer do we pass individual parameters to the SSH Signer funcs, but we now pass an input Struct. This will guard from from additional parameter changes in the future.
We also now add support for using `SDC_*` and `TRITON_*` env vars when working with the Default agent signer
## 0.4.2 (December 22)
- Fixing a panic when the user loses network connectivity when making a GET request to instance [#81]
## 0.4.1 (December 15)
- Clean up the handling of directory sanitization. Use abs paths everywhere [#79]

View file

@ -15,11 +15,17 @@ using a key stored with the local SSH Agent (using an [`SSHAgentSigner`][6].
To construct a Signer, use the `New*` range of methods in the `authentication`
package. In the case of `authentication.NewSSHAgentSigner`, the parameters are
the fingerprint of the key with which to sign, and the account name (normally
stored in the `SDC_ACCOUNT` environment variable). For example:
stored in the `TRITON_ACCOUNT` environment variable). There is also support for
passing in a username, this will allow you to use an account other than the main
Triton account. For example:
```
const fingerprint := "a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11"
sshKeySigner, err := authentication.NewSSHAgentSigner(fingerprint, "AccountName")
```go
input := authentication.SSHAgentSignerInput{
KeyID: "a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11",
AccountName: "AccountName",
Username: "Username",
}
sshKeySigner, err := authentication.NewSSHAgentSigner(input)
if err != nil {
log.Fatalf("NewSSHAgentSigner: %s", err)
}
@ -36,17 +42,18 @@ their own seperate client. In order to initialize a package client, simply pass
the global `triton.ClientConfig` struct into the client's constructor function.
```go
config := &triton.ClientConfig{
TritonURL: os.Getenv("SDC_URL"),
MantaURL: os.Getenv("MANTA_URL"),
AccountName: accountName,
Signers: []authentication.Signer{sshKeySigner},
}
config := &triton.ClientConfig{
TritonURL: os.Getenv("TRITON_URL"),
MantaURL: os.Getenv("MANTA_URL"),
AccountName: accountName,
Username: os.Getenv("TRITON_USER"),
Signers: []authentication.Signer{sshKeySigner},
}
c, err := compute.NewClient(config)
if err != nil {
log.Fatalf("compute.NewClient: %s", err)
}
c, err := compute.NewClient(config)
if err != nil {
log.Fatalf("compute.NewClient: %s", err)
}
```
Constructing `compute.Client` returns an interface which exposes `compute` API
@ -57,10 +64,10 @@ The same `triton.ClientConfig` will initialize the Manta `storage` client as
well...
```go
c, err := storage.NewClient(config)
if err != nil {
log.Fatalf("storage.NewClient: %s", err)
}
c, err := storage.NewClient(config)
if err != nil {
log.Fatalf("storage.NewClient: %s", err)
}
```
## Error Handling
@ -81,13 +88,14 @@ set:
- `TRITON_TEST` - must be set to any value in order to indicate desire to create
resources
- `SDC_URL` - the base endpoint for the Triton API
- `SDC_ACCOUNT` - the account name for the Triton API
- `SDC_KEY_ID` - the fingerprint of the SSH key identifying the key
- `TRITON_URL` - the base endpoint for the Triton API
- `TRITON_ACCOUNT` - the account name for the Triton API
- `TRITON_KEY_ID` - the fingerprint of the SSH key identifying the key
Additionally, you may set `SDC_KEY_MATERIAL` to the contents of an unencrypted
Additionally, you may set `TRITON_KEY_MATERIAL` to the contents of an unencrypted
private key. If this is set, the PrivateKeySigner (see above) will be used - if
not the SSHAgentSigner will be used.
not the SSHAgentSigner will be used. You can also set `TRITON_USER` to run the tests
against an account other than the main Triton account
### Example Run
@ -96,9 +104,9 @@ The verbose output has been removed for brevity here.
```
$ HTTP_PROXY=http://localhost:8888 \
TRITON_TEST=1 \
SDC_URL=https://us-sw-1.api.joyent.com \
SDC_ACCOUNT=AccountName \
SDC_KEY_ID=a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11 \
TRITON_URL=https://us-sw-1.api.joyent.com \
TRITON_ACCOUNT=AccountName \
TRITON_KEY_ID=a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11 \
go test -v -run "TestAccKey"
=== RUN TestAccKey_Create
--- PASS: TestAccKey_Create (12.46s)
@ -118,7 +126,7 @@ referencing your SSH key file use by your active `triton` CLI profile.
```sh
$ eval "$(triton env us-sw-1)"
$ SDC_KEY_FILE=~/.ssh/triton-id_rsa go run examples/compute/instances.go
$ TRITON_KEY_FILE=~/.ssh/triton-id_rsa go run examples/compute/instances.go
```
The following is a complete example of how to initialize the `compute` package
@ -144,15 +152,21 @@ import (
)
func main() {
keyID := os.Getenv("SDC_KEY_ID")
accountName := os.Getenv("SDC_ACCOUNT")
keyMaterial := os.Getenv("SDC_KEY_MATERIAL")
keyID := os.Getenv("TRITON_KEY_ID")
accountName := os.Getenv("TRITON_ACCOUNT")
keyMaterial := os.Getenv("TRITON_KEY_MATERIAL")
userName := os.Getenv("TRITON_USER")
var signer authentication.Signer
var err error
if keyMaterial == "" {
signer, err = authentication.NewSSHAgentSigner(keyID, accountName)
input := authentication.SSHAgentSignerInput{
KeyID: keyID,
AccountName: accountName,
Username: userName,
}
signer, err = authentication.NewSSHAgentSigner(input)
if err != nil {
log.Fatalf("Error Creating SSH Agent Signer: {{err}}", err)
}
@ -180,15 +194,22 @@ func main() {
keyBytes = []byte(keyMaterial)
}
signer, err = authentication.NewPrivateKeySigner(keyID, []byte(keyMaterial), accountName)
input := authentication.PrivateKeySignerInput{
KeyID: keyID,
PrivateKeyMaterial: keyBytes,
AccountName: accountName,
Username: userName,
}
signer, err = authentication.NewPrivateKeySigner(input)
if err != nil {
log.Fatalf("Error Creating SSH Private Key Signer: {{err}}", err)
}
}
config := &triton.ClientConfig{
TritonURL: os.Getenv("SDC_URL"),
TritonURL: os.Getenv("TRITON_URL"),
AccountName: accountName,
Username: userName,
Signers: []authentication.Signer{signer},
}

View file

@ -9,6 +9,7 @@ import (
"encoding/pem"
"errors"
"fmt"
"path"
"strings"
"github.com/hashicorp/errwrap"
@ -20,15 +21,23 @@ type PrivateKeySigner struct {
keyFingerprint string
algorithm string
accountName string
userName string
hashFunc crypto.Hash
privateKey *rsa.PrivateKey
}
func NewPrivateKeySigner(keyFingerprint string, privateKeyMaterial []byte, accountName string) (*PrivateKeySigner, error) {
keyFingerprintMD5 := strings.Replace(keyFingerprint, ":", "", -1)
type PrivateKeySignerInput struct {
KeyID string
PrivateKeyMaterial []byte
AccountName string
Username string
}
block, _ := pem.Decode(privateKeyMaterial)
func NewPrivateKeySigner(input PrivateKeySignerInput) (*PrivateKeySigner, error) {
keyFingerprintMD5 := strings.Replace(input.KeyID, ":", "", -1)
block, _ := pem.Decode(input.PrivateKeyMaterial)
if block == nil {
return nil, errors.New("Error PEM-decoding private key material: nil block received")
}
@ -51,13 +60,17 @@ func NewPrivateKeySigner(keyFingerprint string, privateKeyMaterial []byte, accou
signer := &PrivateKeySigner{
formattedKeyFingerprint: displayKeyFingerprint,
keyFingerprint: keyFingerprint,
accountName: accountName,
keyFingerprint: input.KeyID,
accountName: input.AccountName,
hashFunc: crypto.SHA1,
privateKey: rsakey,
}
if input.Username != "" {
signer.userName = input.Username
}
_, algorithm, err := signer.SignRaw("HelloWorld")
if err != nil {
return nil, fmt.Errorf("Cannot sign using ssh agent: %s", err)
@ -80,7 +93,13 @@ func (s *PrivateKeySigner) Sign(dateHeader string) (string, error) {
}
signedBase64 := base64.StdEncoding.EncodeToString(signed)
keyID := fmt.Sprintf("/%s/keys/%s", s.accountName, s.formattedKeyFingerprint)
var keyID string
if s.userName != "" {
keyID = path.Join("/", s.accountName, "users", s.userName, "keys", s.formattedKeyFingerprint)
} else {
keyID = path.Join("/", s.accountName, "keys", s.formattedKeyFingerprint)
}
return fmt.Sprintf(authorizationHeaderFormat, keyID, "rsa-sha1", headerName, signedBase64), nil
}

View file

@ -8,6 +8,7 @@ import (
"fmt"
"net"
"os"
"path"
"strings"
"github.com/hashicorp/errwrap"
@ -24,13 +25,20 @@ type SSHAgentSigner struct {
keyFingerprint string
algorithm string
accountName string
userName string
keyIdentifier string
agent agent.Agent
key ssh.PublicKey
}
func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, error) {
type SSHAgentSignerInput struct {
KeyID string
AccountName string
Username string
}
func NewSSHAgentSigner(input SSHAgentSignerInput) (*SSHAgentSigner, error) {
sshAgentAddress, agentOk := os.LookupEnv("SSH_AUTH_SOCK")
if !agentOk {
return nil, ErrUnsetEnvVar
@ -44,8 +52,8 @@ func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, err
ag := agent.NewClient(conn)
signer := &SSHAgentSigner{
keyFingerprint: keyFingerprint,
accountName: accountName,
keyFingerprint: input.KeyID,
accountName: input.AccountName,
agent: ag,
}
@ -55,7 +63,12 @@ func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, err
}
signer.key = matchingKey
signer.formattedKeyFingerprint = formatPublicKeyFingerprint(signer.key, true)
signer.keyIdentifier = fmt.Sprintf("/%s/keys/%s", signer.accountName, signer.formattedKeyFingerprint)
if input.Username != "" {
signer.userName = input.Username
signer.keyIdentifier = path.Join("/", signer.accountName, "users", input.Username, "keys", signer.formattedKeyFingerprint)
} else {
signer.keyIdentifier = path.Join("/", signer.accountName, "keys", signer.formattedKeyFingerprint)
}
_, algorithm, err := signer.SignRaw("HelloWorld")
if err != nil {

View file

@ -21,7 +21,7 @@ import (
const nilContext = "nil context"
var (
ErrDefaultAuth = errors.New("default SSH agent authentication requires SDC_KEY_ID and SSH_AUTH_SOCK")
ErrDefaultAuth = errors.New("default SSH agent authentication requires SDC_KEY_ID / TRITON_KEY_ID and SSH_AUTH_SOCK")
ErrAccountName = errors.New("missing account name for Triton/Manta")
ErrMissingURL = errors.New("missing Triton and/or Manta URL")
@ -36,6 +36,7 @@ type Client struct {
TritonURL url.URL
MantaURL url.URL
AccountName string
Username string
}
// New is used to construct a Client in order to make API
@ -81,7 +82,7 @@ func New(tritonURL string, mantaURL string, accountName string, signers ...authe
}
// Default to constructing an SSHAgentSigner if there are no other signers
// passed into NewClient and there's an SDC_KEY_ID and SSH_AUTH_SOCK
// passed into NewClient and there's an TRITON_KEY_ID and SSH_AUTH_SOCK
// available in the user's environ(7).
if len(newClient.Authorizers) == 0 {
if err := newClient.DefaultAuth(); err != nil {
@ -92,21 +93,43 @@ func New(tritonURL string, mantaURL string, accountName string, signers ...authe
return newClient, nil
}
var envPrefixes = []string{"TRITON", "SDC"}
// GetTritonEnv looks up environment variables using the preferred "TRITON"
// prefix, but falls back to the SDC prefix. For example, looking up "USER"
// will search for "TRITON_USER" followed by "SDC_USER". If the environment
// variable is not set, an empty string is returned. GetTritonEnv() is used to
// aid in the transition and deprecation of the SDC_* environment variables.
func GetTritonEnv(name string) string {
for _, prefix := range envPrefixes {
if val, found := os.LookupEnv(prefix + "_" + name); found {
return val
}
}
return ""
}
// initDefaultAuth provides a default key signer for a client. This should only
// be used internally if the client has no other key signer for authenticating
// with Triton. We first look for both `SDC_KEY_ID` and `SSH_AUTH_SOCK` in the
// user's environ(7). If so we default to the SSH agent key signer.
func (c *Client) DefaultAuth() error {
if keyID, keyOk := os.LookupEnv("SDC_KEY_ID"); keyOk {
defaultSigner, err := authentication.NewSSHAgentSigner(keyID, c.AccountName)
tritonKeyId := GetTritonEnv("KEY_ID")
if tritonKeyId != "" {
input := authentication.SSHAgentSignerInput{
KeyID: tritonKeyId,
AccountName: c.AccountName,
Username: c.Username,
}
defaultSigner, err := authentication.NewSSHAgentSigner(input)
if err != nil {
return errwrap.Wrapf("problem initializing NewSSHAgentSigner: {{err}}", err)
}
c.Authorizers = append(c.Authorizers, defaultSigner)
} else {
return ErrDefaultAuth
}
return nil
return ErrDefaultAuth
}
// InsecureSkipTLSVerify turns off TLS verification for the client connection. This

View file

@ -260,7 +260,6 @@ func (s *ObjectsClient) Put(ctx context.Context, input *PutObjectInput) error {
absPath := absFileInput(s.client.AccountName, input.ObjectPath)
if input.ForceInsert {
// IsDir() uses a path relative to the account
absDirName := _AbsCleanPath(path.Dir(string(absPath)))
exists, err := checkDirectoryTreeExists(*s, ctx, absDirName)
if err != nil {

View file

@ -14,5 +14,6 @@ type ClientConfig struct {
TritonURL string
MantaURL string
AccountName string
Username string
Signers []authentication.Signer
}

24
vendor/vendor.json vendored
View file

@ -292,28 +292,28 @@
"revisionTime": "2016-01-12T19:33:35Z"
},
{
"checksumSHA1": "NYs0qvjZwsMZAXMtg2HRiED2cb4=",
"checksumSHA1": "oINoQSRkPinChzwEHr3VatB9++Y=",
"path": "github.com/joyent/triton-go",
"revision": "8365851ee7afcbb4cc1c7ba2e414b242ce0574f1",
"revisionTime": "2017-12-15T19:09:06Z"
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-28T20:20:46Z"
},
{
"checksumSHA1": "Cth7NCLH/HaeKh9ZMRpQtudTEQQ=",
"checksumSHA1": "d6pxw8DLxYehLr92fWZTLEWVws8=",
"path": "github.com/joyent/triton-go/authentication",
"revision": "8365851ee7afcbb4cc1c7ba2e414b242ce0574f1",
"revisionTime": "2017-12-15T19:09:06Z"
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-28T20:20:46Z"
},
{
"checksumSHA1": "3ju04DVaxotpCKBF3Q/0vCSOlec=",
"checksumSHA1": "GCHfn8d1Mhswm7n7IRnT0n/w+dw=",
"path": "github.com/joyent/triton-go/client",
"revision": "8365851ee7afcbb4cc1c7ba2e414b242ce0574f1",
"revisionTime": "2017-12-15T19:09:06Z"
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-28T20:20:46Z"
},
{
"checksumSHA1": "/WtyDZMgstGbBYtQ0f+ZfKMS4v8=",
"checksumSHA1": "PJe3Rs8H466xR8o5audO8oWk44Q=",
"path": "github.com/joyent/triton-go/storage",
"revision": "8365851ee7afcbb4cc1c7ba2e414b242ce0574f1",
"revisionTime": "2017-12-15T19:09:06Z"
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-28T20:20:46Z"
},
{
"path": "github.com/klauspost/cpuid",