Introduce STS client grants API and OPA policy integration (#6168)

This PR introduces two new features

- AWS STS compatible STS API named AssumeRoleWithClientGrants

```
POST /?Action=AssumeRoleWithClientGrants&Token=<jwt>
```

This API endpoint returns temporary access credentials, access
tokens signature types supported by this API

  - RSA keys
  - ECDSA keys

Fetches the required public key from the JWKS endpoints, provides
them as rsa or ecdsa public keys.

- External policy engine support, in this case OPA policy engine

- Credentials are stored on disks
This commit is contained in:
Harshavardhana
2018-10-09 14:00:01 -07:00
committed by kannappanr
parent 16a100b597
commit 54ae364def
76 changed files with 7249 additions and 713 deletions

272
pkg/iam/policy/action.go Normal file
View File

@@ -0,0 +1,272 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"encoding/json"
"fmt"
"github.com/minio/minio/pkg/policy/condition"
"github.com/minio/minio/pkg/wildcard"
)
// Action - policy action.
// Refer https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazons3.html
// for more information about available actions.
type Action string
const (
// AbortMultipartUploadAction - AbortMultipartUpload Rest API action.
AbortMultipartUploadAction Action = "s3:AbortMultipartUpload"
// CreateBucketAction - CreateBucket Rest API action.
CreateBucketAction = "s3:CreateBucket"
// DeleteBucketAction - DeleteBucket Rest API action.
DeleteBucketAction = "s3:DeleteBucket"
// DeleteBucketPolicyAction - DeleteBucketPolicy Rest API action.
DeleteBucketPolicyAction = "s3:DeleteBucketPolicy"
// DeleteObjectAction - DeleteObject Rest API action.
DeleteObjectAction = "s3:DeleteObject"
// GetBucketLocationAction - GetBucketLocation Rest API action.
GetBucketLocationAction = "s3:GetBucketLocation"
// GetBucketNotificationAction - GetBucketNotification Rest API action.
GetBucketNotificationAction = "s3:GetBucketNotification"
// GetBucketPolicyAction - GetBucketPolicy Rest API action.
GetBucketPolicyAction = "s3:GetBucketPolicy"
// GetObjectAction - GetObject Rest API action.
GetObjectAction = "s3:GetObject"
// HeadBucketAction - HeadBucket Rest API action. This action is unused in minio.
HeadBucketAction = "s3:HeadBucket"
// ListAllMyBucketsAction - ListAllMyBuckets (List buckets) Rest API action.
ListAllMyBucketsAction = "s3:ListAllMyBuckets"
// ListBucketAction - ListBucket Rest API action.
ListBucketAction = "s3:ListBucket"
// ListBucketMultipartUploadsAction - ListMultipartUploads Rest API action.
ListBucketMultipartUploadsAction = "s3:ListBucketMultipartUploads"
// ListenBucketNotificationAction - ListenBucketNotification Rest API action.
// This is Minio extension.
ListenBucketNotificationAction = "s3:ListenBucketNotification"
// ListMultipartUploadPartsAction - ListParts Rest API action.
ListMultipartUploadPartsAction = "s3:ListMultipartUploadParts"
// PutBucketNotificationAction - PutObjectNotification Rest API action.
PutBucketNotificationAction = "s3:PutBucketNotification"
// PutBucketPolicyAction - PutBucketPolicy Rest API action.
PutBucketPolicyAction = "s3:PutBucketPolicy"
// PutObjectAction - PutObject Rest API action.
PutObjectAction = "s3:PutObject"
// AllActions - all API actions
AllActions = "s3:*"
)
// List of all supported actions.
var supportedActions = map[Action]struct{}{
AllActions: {},
AbortMultipartUploadAction: {},
CreateBucketAction: {},
DeleteBucketAction: {},
DeleteBucketPolicyAction: {},
DeleteObjectAction: {},
GetBucketLocationAction: {},
GetBucketNotificationAction: {},
GetBucketPolicyAction: {},
GetObjectAction: {},
HeadBucketAction: {},
ListAllMyBucketsAction: {},
ListBucketAction: {},
ListBucketMultipartUploadsAction: {},
ListenBucketNotificationAction: {},
ListMultipartUploadPartsAction: {},
PutBucketNotificationAction: {},
PutBucketPolicyAction: {},
PutObjectAction: {},
}
// isObjectAction - returns whether action is object type or not.
func (action Action) isObjectAction() bool {
switch action {
case AbortMultipartUploadAction, DeleteObjectAction, GetObjectAction:
fallthrough
case ListMultipartUploadPartsAction, PutObjectAction, AllActions:
return true
}
return false
}
// Match - matches object name with resource pattern.
func (action Action) Match(a Action) bool {
return wildcard.Match(string(action), string(a))
}
// IsValid - checks if action is valid or not.
func (action Action) IsValid() bool {
_, ok := supportedActions[action]
return ok
}
// MarshalJSON - encodes Action to JSON data.
func (action Action) MarshalJSON() ([]byte, error) {
if action.IsValid() {
return json.Marshal(string(action))
}
return nil, fmt.Errorf("invalid action '%v'", action)
}
// UnmarshalJSON - decodes JSON data to Action.
func (action *Action) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
a := Action(s)
if !a.IsValid() {
return fmt.Errorf("invalid action '%v'", s)
}
*action = a
return nil
}
func parseAction(s string) (Action, error) {
action := Action(s)
if action.IsValid() {
return action, nil
}
return action, fmt.Errorf("unsupported action '%v'", s)
}
// actionConditionKeyMap - holds mapping of supported condition key for an action.
var actionConditionKeyMap = map[Action]condition.KeySet{
AbortMultipartUploadAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
CreateBucketAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
DeleteBucketPolicyAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
DeleteObjectAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetBucketLocationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetBucketNotificationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetBucketPolicyAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetObjectAction: condition.NewKeySet(
condition.S3XAmzServerSideEncryption,
condition.S3XAmzServerSideEncryptionAwsKMSKeyID,
condition.S3XAmzStorageClass,
condition.AWSReferer,
condition.AWSSourceIP,
),
HeadBucketAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListAllMyBucketsAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListBucketAction: condition.NewKeySet(
condition.S3Prefix,
condition.S3Delimiter,
condition.S3MaxKeys,
condition.AWSReferer,
condition.AWSSourceIP,
),
ListBucketMultipartUploadsAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListenBucketNotificationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListMultipartUploadPartsAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
PutBucketNotificationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
PutBucketPolicyAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
PutObjectAction: condition.NewKeySet(
condition.S3XAmzCopySource,
condition.S3XAmzServerSideEncryption,
condition.S3XAmzServerSideEncryptionAwsKMSKeyID,
condition.S3XAmzMetadataDirective,
condition.S3XAmzStorageClass,
condition.AWSReferer,
condition.AWSSourceIP,
),
}

View File

@@ -0,0 +1,116 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"encoding/json"
"reflect"
"testing"
)
func TestActionIsObjectAction(t *testing.T) {
testCases := []struct {
action Action
expectedResult bool
}{
{AbortMultipartUploadAction, true},
{DeleteObjectAction, true},
{GetObjectAction, true},
{ListMultipartUploadPartsAction, true},
{PutObjectAction, true},
{CreateBucketAction, false},
}
for i, testCase := range testCases {
result := testCase.action.isObjectAction()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestActionIsValid(t *testing.T) {
testCases := []struct {
action Action
expectedResult bool
}{
{AbortMultipartUploadAction, true},
{Action("foo"), false},
}
for i, testCase := range testCases {
result := testCase.action.IsValid()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestActionMarshalJSON(t *testing.T) {
testCases := []struct {
action Action
expectedResult []byte
expectErr bool
}{
{PutObjectAction, []byte(`"s3:PutObject"`), false},
{Action("foo"), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.action)
expectErr := (err != nil)
if testCase.expectErr != expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
}
func TestActionUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Action
expectErr bool
}{
{[]byte(`"s3:PutObject"`), PutObjectAction, false},
{[]byte(`"foo"`), Action(""), true},
}
for i, testCase := range testCases {
var result Action
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if testCase.expectErr != expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if testCase.expectedResult != result {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
}

119
pkg/iam/policy/actionset.go Normal file
View File

@@ -0,0 +1,119 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"encoding/json"
"fmt"
"sort"
"github.com/minio/minio-go/pkg/set"
)
// ActionSet - set of actions.
type ActionSet map[Action]struct{}
// Add - add action to the set.
func (actionSet ActionSet) Add(action Action) {
actionSet[action] = struct{}{}
}
// Match - matches object name with anyone of action pattern in action set.
func (actionSet ActionSet) Match(action Action) bool {
for r := range actionSet {
if r.Match(action) {
return true
}
}
return false
}
// Intersection - returns actions available in both ActionSet.
func (actionSet ActionSet) Intersection(sset ActionSet) ActionSet {
nset := NewActionSet()
for k := range actionSet {
if _, ok := sset[k]; ok {
nset.Add(k)
}
}
return nset
}
// MarshalJSON - encodes ActionSet to JSON data.
func (actionSet ActionSet) MarshalJSON() ([]byte, error) {
if len(actionSet) == 0 {
return nil, fmt.Errorf("empty action set")
}
return json.Marshal(actionSet.ToSlice())
}
func (actionSet ActionSet) String() string {
actions := []string{}
for action := range actionSet {
actions = append(actions, string(action))
}
sort.Strings(actions)
return fmt.Sprintf("%v", actions)
}
// ToSlice - returns slice of actions from the action set.
func (actionSet ActionSet) ToSlice() []Action {
actions := []Action{}
for action := range actionSet {
actions = append(actions, action)
}
return actions
}
// UnmarshalJSON - decodes JSON data to ActionSet.
func (actionSet *ActionSet) UnmarshalJSON(data []byte) error {
var sset set.StringSet
if err := json.Unmarshal(data, &sset); err != nil {
return err
}
if len(sset) == 0 {
return fmt.Errorf("empty action set")
}
*actionSet = make(ActionSet)
for _, s := range sset.ToSlice() {
action, err := parseAction(s)
if err != nil {
return err
}
actionSet.Add(action)
}
return nil
}
// NewActionSet - creates new action set.
func NewActionSet(actions ...Action) ActionSet {
actionSet := make(ActionSet)
for _, action := range actions {
actionSet.Add(action)
}
return actionSet
}

View File

@@ -0,0 +1,159 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"encoding/json"
"reflect"
"testing"
)
func TestActionSetAdd(t *testing.T) {
testCases := []struct {
set ActionSet
action Action
expectedResult ActionSet
}{
{NewActionSet(), PutObjectAction, NewActionSet(PutObjectAction)},
{NewActionSet(PutObjectAction), PutObjectAction, NewActionSet(PutObjectAction)},
}
for i, testCase := range testCases {
testCase.set.Add(testCase.action)
if !reflect.DeepEqual(testCase.expectedResult, testCase.set) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
}
}
}
func TestActionSetMatches(t *testing.T) {
testCases := []struct {
set ActionSet
action Action
expectedResult bool
}{
{NewActionSet(AllActions), AbortMultipartUploadAction, true},
{NewActionSet(PutObjectAction), PutObjectAction, true},
{NewActionSet(PutObjectAction, GetObjectAction), PutObjectAction, true},
{NewActionSet(PutObjectAction, GetObjectAction), AbortMultipartUploadAction, false},
}
for i, testCase := range testCases {
result := testCase.set.Match(testCase.action)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestActionSetIntersection(t *testing.T) {
testCases := []struct {
set ActionSet
setToIntersect ActionSet
expectedResult ActionSet
}{
{NewActionSet(), NewActionSet(PutObjectAction), NewActionSet()},
{NewActionSet(PutObjectAction), NewActionSet(), NewActionSet()},
{NewActionSet(PutObjectAction), NewActionSet(PutObjectAction, GetObjectAction), NewActionSet(PutObjectAction)},
}
for i, testCase := range testCases {
result := testCase.set.Intersection(testCase.setToIntersect)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
}
}
}
func TestActionSetMarshalJSON(t *testing.T) {
testCases := []struct {
actionSet ActionSet
expectedResult []byte
expectErr bool
}{
{NewActionSet(PutObjectAction), []byte(`["s3:PutObject"]`), false},
{NewActionSet(), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.actionSet)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestActionSetToSlice(t *testing.T) {
testCases := []struct {
actionSet ActionSet
expectedResult []Action
}{
{NewActionSet(PutObjectAction), []Action{PutObjectAction}},
{NewActionSet(), []Action{}},
}
for i, testCase := range testCases {
result := testCase.actionSet.ToSlice()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestActionSetUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult ActionSet
expectErr bool
}{
{[]byte(`"s3:PutObject"`), NewActionSet(PutObjectAction), false},
{[]byte(`["s3:PutObject"]`), NewActionSet(PutObjectAction), false},
{[]byte(`["s3:PutObject", "s3:GetObject"]`), NewActionSet(PutObjectAction, GetObjectAction), false},
{[]byte(`["s3:PutObject", "s3:GetObject", "s3:PutObject"]`), NewActionSet(PutObjectAction, GetObjectAction), false},
{[]byte(`[]`), NewActionSet(), true}, // Empty array.
{[]byte(`"foo"`), nil, true}, // Invalid action.
{[]byte(`["s3:PutObject", "foo"]`), nil, true}, // Invalid action.
}
for i, testCase := range testCases {
result := make(ActionSet)
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

172
pkg/iam/policy/opa.go Normal file
View File

@@ -0,0 +1,172 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"bytes"
"crypto/tls"
"encoding/json"
"net"
"net/http"
"os"
"time"
xnet "github.com/minio/minio/pkg/net"
)
// OpaArgs opa general purpose policy engine configuration.
type OpaArgs struct {
URL *xnet.URL `json:"url"`
AuthToken string `json:"authToken"`
}
// Validate - validate opa configuration params.
func (a *OpaArgs) Validate() error {
return nil
}
// UnmarshalJSON - decodes JSON data.
func (a *OpaArgs) UnmarshalJSON(data []byte) error {
// subtype to avoid recursive call to UnmarshalJSON()
type subOpaArgs OpaArgs
var so subOpaArgs
if opaURL, ok := os.LookupEnv("MINIO_IAM_OPA_URL"); ok {
u, err := xnet.ParseURL(opaURL)
if err != nil {
return err
}
so.URL = u
so.AuthToken = os.Getenv("MINIO_IAM_OPA_AUTHTOKEN")
} else {
if err := json.Unmarshal(data, &so); err != nil {
return err
}
}
oa := OpaArgs(so)
if oa.URL == nil || oa.URL.String() == "" {
*a = oa
return nil
}
if err := oa.Validate(); err != nil {
return err
}
*a = oa
return nil
}
// Opa - implements opa policy agent calls.
type Opa struct {
args OpaArgs
secureFailed bool
client *http.Client
insecureClient *http.Client
}
// newCustomHTTPTransport returns a new http configuration
// used while communicating with the cloud backends.
// This sets the value for MaxIdleConnsPerHost from 2 (go default)
// to 100.
func newCustomHTTPTransport(insecure bool) *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 1024,
MaxIdleConnsPerHost: 1024,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
DisableCompression: true,
}
}
// NewOpa - initializes opa policy engine connector.
func NewOpa(args OpaArgs) *Opa {
// No opa args.
if args.URL == nil && args.AuthToken == "" {
return nil
}
return &Opa{
args: args,
client: &http.Client{Transport: newCustomHTTPTransport(false)},
insecureClient: &http.Client{Transport: newCustomHTTPTransport(true)},
}
}
// IsAllowed - checks given policy args is allowed to continue the REST API.
func (o *Opa) IsAllowed(args Args) bool {
if o == nil {
return false
}
// OPA input
body := make(map[string]interface{})
body["input"] = args
inputBytes, err := json.Marshal(body)
if err != nil {
return false
}
req, err := http.NewRequest("POST", o.args.URL.String(), bytes.NewReader(inputBytes))
if err != nil {
return false
}
req.Header.Set("Content-Type", "application/json")
if o.args.AuthToken != "" {
req.Header.Set("Authorization", o.args.AuthToken)
}
var resp *http.Response
if o.secureFailed {
resp, err = o.insecureClient.Do(req)
} else {
resp, err = o.client.Do(req)
if err != nil {
o.secureFailed = true
resp, err = o.insecureClient.Do(req)
if err != nil {
return false
}
}
}
if err != nil {
return false
}
defer resp.Body.Close()
// Handle OPA response
type opaResponse struct {
Result struct {
Allow bool `json:"allow"`
} `json:"result"`
}
var result opaResponse
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false
}
return result.Result.Allow
}

173
pkg/iam/policy/policy.go Normal file
View File

@@ -0,0 +1,173 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"encoding/json"
"fmt"
"io"
"github.com/minio/minio/pkg/policy"
)
// DefaultVersion - default policy version as per AWS S3 specification.
const DefaultVersion = "2012-10-17"
// Args - arguments to policy to check whether it is allowed
type Args struct {
AccountName string `json:"account"`
Action Action `json:"action"`
BucketName string `json:"bucket"`
ConditionValues map[string][]string `json:"conditions"`
IsOwner bool `json:"owner"`
ObjectName string `json:"object"`
Claims map[string]interface{} `json:"claims"`
}
// Policy - iam bucket iamp.
type Policy struct {
ID policy.ID `json:"ID,omitempty"`
Version string
Statements []Statement `json:"Statement"`
}
// IsAllowed - checks given policy args is allowed to continue the Rest API.
func (iamp Policy) IsAllowed(args Args) bool {
// Check all deny statements. If any one statement denies, return false.
for _, statement := range iamp.Statements {
if statement.Effect == policy.Deny {
if !statement.IsAllowed(args) {
return false
}
}
}
// For owner, its allowed by default.
if args.IsOwner {
return true
}
// Check all allow statements. If any one statement allows, return true.
for _, statement := range iamp.Statements {
if statement.Effect == policy.Allow {
if statement.IsAllowed(args) {
return true
}
}
}
return false
}
// IsEmpty - returns whether policy is empty or not.
func (iamp Policy) IsEmpty() bool {
return len(iamp.Statements) == 0
}
// isValid - checks if Policy is valid or not.
func (iamp Policy) isValid() error {
if iamp.Version != DefaultVersion && iamp.Version != "" {
return fmt.Errorf("invalid version '%v'", iamp.Version)
}
for _, statement := range iamp.Statements {
if err := statement.isValid(); err != nil {
return err
}
}
for i := range iamp.Statements {
for _, statement := range iamp.Statements[i+1:] {
actions := iamp.Statements[i].Actions.Intersection(statement.Actions)
if len(actions) == 0 {
continue
}
resources := iamp.Statements[i].Resources.Intersection(statement.Resources)
if len(resources) == 0 {
continue
}
if iamp.Statements[i].Conditions.String() != statement.Conditions.String() {
continue
}
return fmt.Errorf("duplicate actions %v, resources %v found in statements %v, %v",
actions, resources, iamp.Statements[i], statement)
}
}
return nil
}
// MarshalJSON - encodes Policy to JSON data.
func (iamp Policy) MarshalJSON() ([]byte, error) {
if err := iamp.isValid(); err != nil {
return nil, err
}
// subtype to avoid recursive call to MarshalJSON()
type subPolicy Policy
return json.Marshal(subPolicy(iamp))
}
// UnmarshalJSON - decodes JSON data to Iamp.
func (iamp *Policy) UnmarshalJSON(data []byte) error {
// subtype to avoid recursive call to UnmarshalJSON()
type subPolicy Policy
var sp subPolicy
if err := json.Unmarshal(data, &sp); err != nil {
return err
}
p := Policy(sp)
if err := p.isValid(); err != nil {
return err
}
*iamp = p
return nil
}
// Validate - validates all statements are for given bucket or not.
func (iamp Policy) Validate() error {
if err := iamp.isValid(); err != nil {
return err
}
for _, statement := range iamp.Statements {
if err := statement.Validate(); err != nil {
return err
}
}
return nil
}
// ParseConfig - parses data in given reader to Iamp.
func ParseConfig(reader io.Reader) (*Policy, error) {
var iamp Policy
decoder := json.NewDecoder(reader)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&iamp); err != nil {
return nil, err
}
return &iamp, iamp.Validate()
}

File diff suppressed because it is too large Load Diff

129
pkg/iam/policy/resource.go Normal file
View File

@@ -0,0 +1,129 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"encoding/json"
"fmt"
"strings"
"github.com/minio/minio/pkg/wildcard"
)
// ResourceARNPrefix - resource ARN prefix as per AWS S3 specification.
const ResourceARNPrefix = "arn:aws:s3:::"
// Resource - resource in policy statement.
type Resource struct {
BucketName string
Pattern string
}
func (r Resource) isBucketPattern() bool {
return !strings.Contains(r.Pattern, "/") || r.Pattern == "*"
}
func (r Resource) isObjectPattern() bool {
return strings.Contains(r.Pattern, "/") || strings.Contains(r.BucketName, "*") || r.Pattern == "*/*"
}
// IsValid - checks whether Resource is valid or not.
func (r Resource) IsValid() bool {
return r.Pattern != ""
}
// Match - matches object name with resource pattern.
func (r Resource) Match(resource string) bool {
if strings.HasPrefix(resource, r.Pattern) {
return true
}
return wildcard.Match(r.Pattern, resource)
}
// MarshalJSON - encodes Resource to JSON data.
func (r Resource) MarshalJSON() ([]byte, error) {
if !r.IsValid() {
return nil, fmt.Errorf("invalid resource %v", r)
}
return json.Marshal(r.String())
}
func (r Resource) String() string {
return ResourceARNPrefix + r.Pattern
}
// UnmarshalJSON - decodes JSON data to Resource.
func (r *Resource) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsedResource, err := parseResource(s)
if err != nil {
return err
}
*r = parsedResource
return nil
}
// Validate - validates Resource is for given bucket or not.
func (r Resource) Validate() error {
if !r.IsValid() {
return fmt.Errorf("invalid resource")
}
return nil
}
// parseResource - parses string to Resource.
func parseResource(s string) (Resource, error) {
if !strings.HasPrefix(s, ResourceARNPrefix) {
return Resource{}, fmt.Errorf("invalid resource '%v'", s)
}
pattern := strings.TrimPrefix(s, ResourceARNPrefix)
tokens := strings.SplitN(pattern, "/", 2)
bucketName := tokens[0]
if bucketName == "" {
return Resource{}, fmt.Errorf("invalid resource format '%v'", s)
}
return Resource{
BucketName: bucketName,
Pattern: pattern,
}, nil
}
// NewResource - creates new resource.
func NewResource(bucketName, keyName string) Resource {
pattern := bucketName
if keyName != "" {
if !strings.HasPrefix(keyName, "/") {
pattern += "/"
}
pattern += keyName
}
return Resource{
BucketName: bucketName,
Pattern: pattern,
}
}

View File

@@ -0,0 +1,220 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"encoding/json"
"reflect"
"testing"
)
func TestResourceIsBucketPattern(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult bool
}{
{NewResource("*", ""), true},
{NewResource("mybucket", ""), true},
{NewResource("mybucket*", ""), true},
{NewResource("mybucket?0", ""), true},
{NewResource("", "*"), false},
{NewResource("*", "*"), false},
{NewResource("mybucket", "*"), false},
{NewResource("mybucket*", "/myobject"), false},
{NewResource("mybucket?0", "/2010/photos/*"), false},
}
for i, testCase := range testCases {
result := testCase.resource.isBucketPattern()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceIsObjectPattern(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult bool
}{
{NewResource("*", ""), true},
{NewResource("mybucket*", ""), true},
{NewResource("", "*"), true},
{NewResource("*", "*"), true},
{NewResource("mybucket", "*"), true},
{NewResource("mybucket*", "/myobject"), true},
{NewResource("mybucket?0", "/2010/photos/*"), true},
{NewResource("mybucket", ""), false},
{NewResource("mybucket?0", ""), false},
}
for i, testCase := range testCases {
result := testCase.resource.isObjectPattern()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceIsValid(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult bool
}{
{NewResource("*", ""), true},
{NewResource("mybucket*", ""), true},
{NewResource("*", "*"), true},
{NewResource("mybucket", "*"), true},
{NewResource("mybucket*", "/myobject"), true},
{NewResource("mybucket?0", "/2010/photos/*"), true},
{NewResource("mybucket", ""), true},
{NewResource("mybucket?0", ""), true},
{NewResource("", "*"), true},
{NewResource("", ""), false},
}
for i, testCase := range testCases {
result := testCase.resource.IsValid()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceMatch(t *testing.T) {
testCases := []struct {
resource Resource
objectName string
expectedResult bool
}{
{NewResource("*", ""), "mybucket", true},
{NewResource("*", ""), "mybucket/myobject", true},
{NewResource("mybucket*", ""), "mybucket", true},
{NewResource("mybucket*", ""), "mybucket/myobject", true},
{NewResource("", "*"), "/myobject", true},
{NewResource("*", "*"), "mybucket/myobject", true},
{NewResource("mybucket", "*"), "mybucket/myobject", true},
{NewResource("mybucket*", "/myobject"), "mybucket/myobject", true},
{NewResource("mybucket*", "/myobject"), "mybucket100/myobject", true},
{NewResource("mybucket?0", "/2010/photos/*"), "mybucket20/2010/photos/1.jpg", true},
{NewResource("mybucket", ""), "mybucket", true},
{NewResource("mybucket?0", ""), "mybucket30", true},
{NewResource("", "*"), "mybucket/myobject", false},
{NewResource("*", "*"), "mybucket", false},
{NewResource("mybucket", "*"), "mybucket10/myobject", false},
{NewResource("mybucket?0", "/2010/photos/*"), "mybucket0/2010/photos/1.jpg", false},
{NewResource("mybucket", ""), "mybucket/myobject", true},
}
for i, testCase := range testCases {
result := testCase.resource.Match(testCase.objectName)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceMarshalJSON(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult []byte
expectErr bool
}{
{NewResource("*", ""), []byte(`"arn:aws:s3:::*"`), false},
{NewResource("mybucket*", ""), []byte(`"arn:aws:s3:::mybucket*"`), false},
{NewResource("mybucket", ""), []byte(`"arn:aws:s3:::mybucket"`), false},
{NewResource("*", "*"), []byte(`"arn:aws:s3:::*/*"`), false},
{NewResource("", "*"), []byte(`"arn:aws:s3:::/*"`), false},
{NewResource("mybucket", "*"), []byte(`"arn:aws:s3:::mybucket/*"`), false},
{NewResource("mybucket*", "myobject"), []byte(`"arn:aws:s3:::mybucket*/myobject"`), false},
{NewResource("mybucket?0", "/2010/photos/*"), []byte(`"arn:aws:s3:::mybucket?0/2010/photos/*"`), false},
{Resource{}, nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.resource)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestResourceUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Resource
expectErr bool
}{
{[]byte(`"arn:aws:s3:::*"`), NewResource("*", ""), false},
{[]byte(`"arn:aws:s3:::mybucket*"`), NewResource("mybucket*", ""), false},
{[]byte(`"arn:aws:s3:::mybucket"`), NewResource("mybucket", ""), false},
{[]byte(`"arn:aws:s3:::*/*"`), NewResource("*", "*"), false},
{[]byte(`"arn:aws:s3:::mybucket/*"`), NewResource("mybucket", "*"), false},
{[]byte(`"arn:aws:s3:::mybucket*/myobject"`), NewResource("mybucket*", "myobject"), false},
{[]byte(`"arn:aws:s3:::mybucket?0/2010/photos/*"`), NewResource("mybucket?0", "/2010/photos/*"), false},
{[]byte(`"mybucket/myobject*"`), Resource{}, true},
{[]byte(`"arn:aws:s3:::/*"`), Resource{}, true},
}
for i, testCase := range testCases {
var result Resource
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
}
func TestResourceValidate(t *testing.T) {
testCases := []struct {
resource Resource
expectErr bool
}{
{NewResource("mybucket", "/myobject*"), false},
{NewResource("", "/myobject*"), false},
{NewResource("", ""), true},
}
for i, testCase := range testCases {
err := testCase.resource.Validate()
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}

View File

@@ -0,0 +1,147 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"encoding/json"
"fmt"
"sort"
"github.com/minio/minio-go/pkg/set"
)
// ResourceSet - set of resources in policy statement.
type ResourceSet map[Resource]struct{}
// bucketResourceExists - checks if at least one bucket resource exists in the set.
func (resourceSet ResourceSet) bucketResourceExists() bool {
for resource := range resourceSet {
if resource.isBucketPattern() {
return true
}
}
return false
}
// objectResourceExists - checks if at least one object resource exists in the set.
func (resourceSet ResourceSet) objectResourceExists() bool {
for resource := range resourceSet {
if resource.isObjectPattern() {
return true
}
}
return false
}
// Add - adds resource to resource set.
func (resourceSet ResourceSet) Add(resource Resource) {
resourceSet[resource] = struct{}{}
}
// Intersection - returns resources available in both ResourceSet.
func (resourceSet ResourceSet) Intersection(sset ResourceSet) ResourceSet {
nset := NewResourceSet()
for k := range resourceSet {
if _, ok := sset[k]; ok {
nset.Add(k)
}
}
return nset
}
// MarshalJSON - encodes ResourceSet to JSON data.
func (resourceSet ResourceSet) MarshalJSON() ([]byte, error) {
if len(resourceSet) == 0 {
return nil, fmt.Errorf("empty resource set")
}
resources := []Resource{}
for resource := range resourceSet {
resources = append(resources, resource)
}
return json.Marshal(resources)
}
// Match - matches object name with anyone of resource pattern in resource set.
func (resourceSet ResourceSet) Match(resource string) bool {
for r := range resourceSet {
if r.Match(resource) {
return true
}
}
return false
}
func (resourceSet ResourceSet) String() string {
resources := []string{}
for resource := range resourceSet {
resources = append(resources, resource.String())
}
sort.Strings(resources)
return fmt.Sprintf("%v", resources)
}
// UnmarshalJSON - decodes JSON data to ResourceSet.
func (resourceSet *ResourceSet) UnmarshalJSON(data []byte) error {
var sset set.StringSet
if err := json.Unmarshal(data, &sset); err != nil {
return err
}
*resourceSet = make(ResourceSet)
for _, s := range sset.ToSlice() {
resource, err := parseResource(s)
if err != nil {
return err
}
if _, found := (*resourceSet)[resource]; found {
return fmt.Errorf("duplicate resource '%v' found", s)
}
resourceSet.Add(resource)
}
return nil
}
// Validate - validates ResourceSet.
func (resourceSet ResourceSet) Validate() error {
for resource := range resourceSet {
if err := resource.Validate(); err != nil {
return err
}
}
return nil
}
// NewResourceSet - creates new resource set.
func NewResourceSet(resources ...Resource) ResourceSet {
resourceSet := make(ResourceSet)
for _, resource := range resources {
resourceSet.Add(resource)
}
return resourceSet
}

View File

@@ -0,0 +1,239 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"encoding/json"
"reflect"
"testing"
)
func TestResourceSetBucketResourceExists(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
expectedResult bool
}{
{NewResourceSet(NewResource("*", "")), true},
{NewResourceSet(NewResource("mybucket", "")), true},
{NewResourceSet(NewResource("mybucket*", "")), true},
{NewResourceSet(NewResource("mybucket?0", "")), true},
{NewResourceSet(NewResource("mybucket", "/2010/photos/*"), NewResource("mybucket", "")), true},
{NewResourceSet(NewResource("", "*")), false},
{NewResourceSet(NewResource("*", "*")), false},
{NewResourceSet(NewResource("mybucket", "*")), false},
{NewResourceSet(NewResource("mybucket*", "/myobject")), false},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), false},
}
for i, testCase := range testCases {
result := testCase.resourceSet.bucketResourceExists()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceSetObjectResourceExists(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
expectedResult bool
}{
{NewResourceSet(NewResource("*", "")), true},
{NewResourceSet(NewResource("mybucket*", "")), true},
{NewResourceSet(NewResource("", "*")), true},
{NewResourceSet(NewResource("*", "*")), true},
{NewResourceSet(NewResource("mybucket", "*")), true},
{NewResourceSet(NewResource("mybucket*", "/myobject")), true},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), true},
{NewResourceSet(NewResource("mybucket", ""), NewResource("mybucket", "/2910/photos/*")), true},
{NewResourceSet(NewResource("mybucket", "")), false},
{NewResourceSet(NewResource("mybucket?0", "")), false},
}
for i, testCase := range testCases {
result := testCase.resourceSet.objectResourceExists()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceSetAdd(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
resource Resource
expectedResult ResourceSet
}{
{NewResourceSet(), NewResource("mybucket", "/myobject*"),
NewResourceSet(NewResource("mybucket", "/myobject*"))},
{NewResourceSet(NewResource("mybucket", "/myobject*")),
NewResource("mybucket", "/yourobject*"),
NewResourceSet(NewResource("mybucket", "/myobject*"),
NewResource("mybucket", "/yourobject*"))},
{NewResourceSet(NewResource("mybucket", "/myobject*")),
NewResource("mybucket", "/myobject*"),
NewResourceSet(NewResource("mybucket", "/myobject*"))},
}
for i, testCase := range testCases {
testCase.resourceSet.Add(testCase.resource)
if !reflect.DeepEqual(testCase.resourceSet, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, testCase.resourceSet)
}
}
}
func TestResourceSetIntersection(t *testing.T) {
testCases := []struct {
set ResourceSet
setToIntersect ResourceSet
expectedResult ResourceSet
}{
{NewResourceSet(), NewResourceSet(NewResource("mybucket", "/myobject*")), NewResourceSet()},
{NewResourceSet(NewResource("mybucket", "/myobject*")), NewResourceSet(), NewResourceSet()},
{NewResourceSet(NewResource("mybucket", "/myobject*")),
NewResourceSet(NewResource("mybucket", "/myobject*"), NewResource("mybucket", "/yourobject*")),
NewResourceSet(NewResource("mybucket", "/myobject*"))},
}
for i, testCase := range testCases {
result := testCase.set.Intersection(testCase.setToIntersect)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
}
}
}
func TestResourceSetMarshalJSON(t *testing.T) {
testCases := []struct {
resoruceSet ResourceSet
expectedResult []byte
expectErr bool
}{
{NewResourceSet(NewResource("mybucket", "/myobject*")),
[]byte(`["arn:aws:s3:::mybucket/myobject*"]`), false},
{NewResourceSet(NewResource("mybucket", "/photos/myobject*")),
[]byte(`["arn:aws:s3:::mybucket/photos/myobject*"]`), false},
{NewResourceSet(), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.resoruceSet)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestResourceSetMatch(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
resource string
expectedResult bool
}{
{NewResourceSet(NewResource("*", "")), "mybucket", true},
{NewResourceSet(NewResource("*", "")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket*", "")), "mybucket", true},
{NewResourceSet(NewResource("mybucket*", "")), "mybucket/myobject", true},
{NewResourceSet(NewResource("", "*")), "/myobject", true},
{NewResourceSet(NewResource("*", "*")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket", "*")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket*", "/myobject")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket*", "/myobject")), "mybucket100/myobject", true},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), "mybucket20/2010/photos/1.jpg", true},
{NewResourceSet(NewResource("mybucket", "")), "mybucket", true},
{NewResourceSet(NewResource("mybucket?0", "")), "mybucket30", true},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*"),
NewResource("mybucket", "/2010/photos/*")), "mybucket/2010/photos/1.jpg", true},
{NewResourceSet(NewResource("", "*")), "mybucket/myobject", false},
{NewResourceSet(NewResource("*", "*")), "mybucket", false},
{NewResourceSet(NewResource("mybucket", "*")), "mybucket10/myobject", false},
{NewResourceSet(NewResource("mybucket", "")), "mybucket/myobject", true},
{NewResourceSet(), "mybucket/myobject", false},
}
for i, testCase := range testCases {
result := testCase.resourceSet.Match(testCase.resource)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceSetUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult ResourceSet
expectErr bool
}{
{[]byte(`"arn:aws:s3:::mybucket/myobject*"`),
NewResourceSet(NewResource("mybucket", "/myobject*")), false},
{[]byte(`"arn:aws:s3:::mybucket/photos/myobject*"`),
NewResourceSet(NewResource("mybucket", "/photos/myobject*")), false},
{[]byte(`"arn:aws:s3:::mybucket"`), NewResourceSet(NewResource("mybucket", "")), false},
{[]byte(`"mybucket/myobject*"`), nil, true},
}
for i, testCase := range testCases {
var result ResourceSet
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
}
func TestResourceSetValidate(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
expectErr bool
}{
{NewResourceSet(NewResource("mybucket", "/myobject*")), false},
{NewResourceSet(NewResource("", "/myobject*")), false},
{NewResourceSet(NewResource("", "")), true},
}
for i, testCase := range testCases {
err := testCase.resourceSet.Validate()
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}

141
pkg/iam/policy/statement.go Normal file
View File

@@ -0,0 +1,141 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"encoding/json"
"fmt"
"strings"
"github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/policy/condition"
)
// Statement - iam policy statement.
type Statement struct {
SID policy.ID `json:"Sid,omitempty"`
Effect policy.Effect `json:"Effect"`
Actions ActionSet `json:"Action"`
Resources ResourceSet `json:"Resource"`
Conditions condition.Functions `json:"Condition,omitempty"`
}
// IsAllowed - checks given policy args is allowed to continue the Rest API.
func (statement Statement) IsAllowed(args Args) bool {
check := func() bool {
if !statement.Actions.Match(args.Action) {
return false
}
resource := args.BucketName
if args.ObjectName != "" {
if !strings.HasPrefix(args.ObjectName, "/") {
resource += "/"
}
resource += args.ObjectName
}
if !statement.Resources.Match(resource) {
return false
}
return statement.Conditions.Evaluate(args.ConditionValues)
}
return statement.Effect.IsAllowed(check())
}
// isValid - checks whether statement is valid or not.
func (statement Statement) isValid() error {
if !statement.Effect.IsValid() {
return fmt.Errorf("invalid Effect %v", statement.Effect)
}
if len(statement.Actions) == 0 {
return fmt.Errorf("Action must not be empty")
}
if len(statement.Resources) == 0 {
return fmt.Errorf("Resource must not be empty")
}
if err := statement.Resources.Validate(); err != nil {
return err
}
for action := range statement.Actions {
if !statement.Resources.objectResourceExists() && !statement.Resources.bucketResourceExists() {
return fmt.Errorf("unsupported Resource found %v for action %v", statement.Resources, action)
}
keys := statement.Conditions.Keys()
keyDiff := keys.Difference(actionConditionKeyMap[action])
if !keyDiff.IsEmpty() {
return fmt.Errorf("unsupported condition keys '%v' used for action '%v'", keyDiff, action)
}
}
return nil
}
// MarshalJSON - encodes JSON data to Statement.
func (statement Statement) MarshalJSON() ([]byte, error) {
if err := statement.isValid(); err != nil {
return nil, err
}
// subtype to avoid recursive call to MarshalJSON()
type subStatement Statement
ss := subStatement(statement)
return json.Marshal(ss)
}
// UnmarshalJSON - decodes JSON data to Statement.
func (statement *Statement) UnmarshalJSON(data []byte) error {
// subtype to avoid recursive call to UnmarshalJSON()
type subStatement Statement
var ss subStatement
if err := json.Unmarshal(data, &ss); err != nil {
return err
}
s := Statement(ss)
if err := s.isValid(); err != nil {
return err
}
*statement = s
return nil
}
// Validate - validates Statement is for given bucket or not.
func (statement Statement) Validate() error {
return statement.isValid()
}
// NewStatement - creates new statement.
func NewStatement(effect policy.Effect, actionSet ActionSet, resourceSet ResourceSet, conditions condition.Functions) Statement {
return Statement{
Effect: effect,
Actions: actionSet,
Resources: resourceSet,
Conditions: conditions,
}
}

View File

@@ -0,0 +1,515 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"encoding/json"
"net"
"reflect"
"testing"
"github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/policy/condition"
)
func TestStatementIsAllowed(t *testing.T) {
case1Statement := NewStatement(
policy.Allow,
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("*", "")),
condition.NewFunctions(),
)
case2Statement := NewStatement(
policy.Allow,
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
_, IPNet1, err := net.ParseCIDR("192.168.1.0/24")
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func1, err := condition.NewIPAddressFunc(
condition.AWSSourceIP,
IPNet1,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Statement := NewStatement(
policy.Allow,
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
case4Statement := NewStatement(
policy.Deny,
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
anonGetBucketLocationArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetBucketLocationAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
}
anonPutObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: PutObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
},
ObjectName: "myobject",
}
anonGetObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
ObjectName: "myobject",
}
getBucketLocationArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetBucketLocationAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
}
putObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: PutObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
},
ObjectName: "myobject",
}
getObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
ObjectName: "myobject",
}
testCases := []struct {
statement Statement
args Args
expectedResult bool
}{
{case1Statement, anonGetBucketLocationArgs, true},
{case1Statement, anonPutObjectActionArgs, true},
{case1Statement, anonGetObjectActionArgs, false},
{case1Statement, getBucketLocationArgs, true},
{case1Statement, putObjectActionArgs, true},
{case1Statement, getObjectActionArgs, false},
{case2Statement, anonGetBucketLocationArgs, false},
{case2Statement, anonPutObjectActionArgs, true},
{case2Statement, anonGetObjectActionArgs, true},
{case2Statement, getBucketLocationArgs, false},
{case2Statement, putObjectActionArgs, true},
{case2Statement, getObjectActionArgs, true},
{case3Statement, anonGetBucketLocationArgs, false},
{case3Statement, anonPutObjectActionArgs, true},
{case3Statement, anonGetObjectActionArgs, false},
{case3Statement, getBucketLocationArgs, false},
{case3Statement, putObjectActionArgs, true},
{case3Statement, getObjectActionArgs, false},
{case4Statement, anonGetBucketLocationArgs, true},
{case4Statement, anonPutObjectActionArgs, false},
{case4Statement, anonGetObjectActionArgs, true},
{case4Statement, getBucketLocationArgs, true},
{case4Statement, putObjectActionArgs, false},
{case4Statement, getObjectActionArgs, true},
}
for i, testCase := range testCases {
result := testCase.statement.IsAllowed(testCase.args)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStatementIsValid(t *testing.T) {
_, IPNet1, err := net.ParseCIDR("192.168.1.0/24")
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func1, err := condition.NewIPAddressFunc(
condition.AWSSourceIP,
IPNet1,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := condition.NewStringEqualsFunc(
condition.S3XAmzCopySource,
"mybucket/myobject",
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
statement Statement
expectErr bool
}{
// Invalid effect error.
{NewStatement(
policy.Effect("foo"),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("*", "")),
condition.NewFunctions(),
), true},
// Empty actions error.
{NewStatement(
policy.Allow,
NewActionSet(),
NewResourceSet(NewResource("*", "")),
condition.NewFunctions(),
), true},
// Empty resources error.
{NewStatement(
policy.Allow,
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(),
condition.NewFunctions(),
), true},
// Unsupported conditions for GetObject
{NewStatement(
policy.Allow,
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1, func2),
), true},
{NewStatement(
policy.Allow,
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(),
), false},
{NewStatement(
policy.Allow,
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "")),
condition.NewFunctions(),
), false},
{NewStatement(
policy.Deny,
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1),
), false},
}
for i, testCase := range testCases {
err := testCase.statement.isValid()
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}
func TestStatementMarshalJSON(t *testing.T) {
case1Statement := NewStatement(
policy.Allow,
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
case1Statement.SID = "SomeId1"
case1Data := []byte(`{"Sid":"SomeId1","Effect":"Allow","Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"]}`)
func1, err := condition.NewNullFunc(
condition.S3XAmzCopySource,
true,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Statement := NewStatement(
policy.Allow,
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
case2Data := []byte(`{"Effect":"Allow","Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-copy-source":[true]}}}`)
func2, err := condition.NewNullFunc(
condition.S3XAmzServerSideEncryption,
false,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Statement := NewStatement(
policy.Deny,
NewActionSet(GetObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func2),
)
case3Data := []byte(`{"Effect":"Deny","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-server-side-encryption":[false]}}}`)
case4Statement := NewStatement(
policy.Allow,
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1, func2),
)
testCases := []struct {
statement Statement
expectedResult []byte
expectErr bool
}{
{case1Statement, case1Data, false},
{case2Statement, case2Data, false},
{case3Statement, case3Data, false},
// Invalid statement error.
{case4Statement, nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.statement)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestStatementUnmarshalJSON(t *testing.T) {
case1Data := []byte(`{
"Sid": "SomeId1",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case1Statement := NewStatement(
policy.Allow,
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
case1Statement.SID = "SomeId1"
case2Data := []byte(`{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
"Null": {
"s3:x-amz-copy-source": true
}
}
}`)
func1, err := condition.NewNullFunc(
condition.S3XAmzCopySource,
true,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Statement := NewStatement(
policy.Allow,
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
case3Data := []byte(`{
"Effect": "Deny",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "false"
}
}
}`)
func2, err := condition.NewNullFunc(
condition.S3XAmzServerSideEncryption,
false,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Statement := NewStatement(
policy.Deny,
NewActionSet(PutObjectAction, GetObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func2),
)
case4Data := []byte(`{
"Effect": "Allow",
"Action": "s3:PutObjec",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case5Data := []byte(`{
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case7Data := []byte(`{
"Effect": "Allow",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case8Data := []byte(`{
"Effect": "Allow",
"Action": "s3:PutObject"
}`)
case9Data := []byte(`{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
}
}`)
case10Data := []byte(`{
"Effect": "Deny",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
"StringEquals": {
"s3:x-amz-copy-source": "yourbucket/myobject*"
}
}
}`)
testCases := []struct {
data []byte
expectedResult Statement
expectErr bool
}{
{case1Data, case1Statement, false},
{case2Data, case2Statement, false},
{case3Data, case3Statement, false},
// JSON unmarshaling error.
{case4Data, Statement{}, true},
// Invalid effect error.
{case5Data, Statement{}, true},
// Empty action error.
{case7Data, Statement{}, true},
// Empty resource error.
{case8Data, Statement{}, true},
// Empty condition error.
{case9Data, Statement{}, true},
// Unsupported condition key error.
{case10Data, Statement{}, true},
}
for i, testCase := range testCases {
var result Statement
expectErr := (json.Unmarshal(testCase.data, &result) != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
}
func TestStatementValidate(t *testing.T) {
case1Statement := NewStatement(
policy.Allow,
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
func1, err := condition.NewNullFunc(
condition.S3XAmzCopySource,
true,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := condition.NewNullFunc(
condition.S3XAmzServerSideEncryption,
false,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Statement := NewStatement(
policy.Allow,
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1, func2),
)
testCases := []struct {
statement Statement
expectErr bool
}{
{case1Statement, false},
{case2Statement, true},
}
for i, testCase := range testCases {
err := testCase.statement.Validate()
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}