diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index 8fa0f77d3..a5200311a 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -1198,6 +1198,16 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ } // InfoCannedPolicy - GET /minio/admin/v3/info-canned-policy?name={policyName} +// +// Newer API response with policy timestamps is returned with query parameter +// `v=2` like: +// +// GET /minio/admin/v3/info-canned-policy?name={policyName}&v=2 +// +// The newer API will eventually become the default (and only) one. The older +// response is to return only the policy JSON. The newer response returns +// timestamps along with the policy JSON. Both versions are supported for now, +// for smooth transition to new API. func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "InfoCannedPolicy") @@ -1208,13 +1218,36 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques return } - policy, err := globalIAMSys.InfoPolicy(mux.Vars(r)["name"]) + name := mux.Vars(r)["name"] + policies := newMappedPolicy(name).toSlice() + if len(policies) != 1 { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errTooManyPolicies), r.URL) + return + } + + policyDoc, err := globalIAMSys.InfoPolicy(name) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } - buf, err := json.MarshalIndent(policy, "", " ") + // Is the new API version being requested? + infoPolicyAPIVersion := r.Form.Get("v") + if infoPolicyAPIVersion == "2" { + buf, err := json.MarshalIndent(policyDoc, "", " ") + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + w.Write(buf) + return + } else if infoPolicyAPIVersion != "" { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errors.New("invalid version parameter 'v' supplied")), r.URL) + return + } + + // Return the older API response value of just the policy json. + buf, err := json.MarshalIndent(policyDoc.Policy, "", " ") if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return diff --git a/cmd/config-common.go b/cmd/config-common.go index 250da471b..33188a3d4 100644 --- a/cmd/config-common.go +++ b/cmd/config-common.go @@ -29,26 +29,31 @@ import ( var errConfigNotFound = errors.New("config file not found") -func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]byte, error) { +func readConfigWithMetadata(ctx context.Context, objAPI ObjectLayer, configFile string) ([]byte, ObjectInfo, error) { r, err := objAPI.GetObjectNInfo(ctx, minioMetaBucket, configFile, nil, http.Header{}, readLock, ObjectOptions{}) if err != nil { // Treat object not found as config not found. if isErrObjectNotFound(err) { - return nil, errConfigNotFound + return nil, ObjectInfo{}, errConfigNotFound } - return nil, err + return nil, ObjectInfo{}, err } defer r.Close() buf, err := ioutil.ReadAll(r) if err != nil { - return nil, err + return nil, ObjectInfo{}, err } if len(buf) == 0 { - return nil, errConfigNotFound + return nil, ObjectInfo{}, errConfigNotFound } - return buf, nil + return buf, r.ObjInfo, nil +} + +func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]byte, error) { + buf, _, err := readConfigWithMetadata(ctx, objAPI, configFile) + return buf, err } type objectDeleter interface { diff --git a/cmd/iam-dummy-store.go b/cmd/iam-dummy-store.go index 3a4b1c644..f70a19d5f 100644 --- a/cmd/iam-dummy-store.go +++ b/cmd/iam-dummy-store.go @@ -22,7 +22,6 @@ import ( "sync" "github.com/minio/minio/internal/auth" - iampolicy "github.com/minio/pkg/iam/policy" ) type iamDummyStore struct { @@ -64,7 +63,7 @@ func (ids *iamDummyStore) migrateBackendFormat(context.Context) error { return nil } -func (ids *iamDummyStore) loadPolicyDoc(ctx context.Context, policy string, m map[string]iampolicy.Policy) error { +func (ids *iamDummyStore) loadPolicyDoc(ctx context.Context, policy string, m map[string]PolicyDoc) error { v, ok := ids.iamPolicyDocsMap[policy] if !ok { return errNoSuchPolicy @@ -73,7 +72,7 @@ func (ids *iamDummyStore) loadPolicyDoc(ctx context.Context, policy string, m ma return nil } -func (ids *iamDummyStore) loadPolicyDocs(ctx context.Context, m map[string]iampolicy.Policy) error { +func (ids *iamDummyStore) loadPolicyDocs(ctx context.Context, m map[string]PolicyDoc) error { for k, v := range ids.iamPolicyDocsMap { m[k] = v } @@ -154,7 +153,7 @@ func (ids *iamDummyStore) deleteIAMConfig(ctx context.Context, path string) erro return nil } -func (ids *iamDummyStore) savePolicyDoc(ctx context.Context, policyName string, p iampolicy.Policy) error { +func (ids *iamDummyStore) savePolicyDoc(ctx context.Context, policyName string, p PolicyDoc) error { return nil } diff --git a/cmd/iam-etcd-store.go b/cmd/iam-etcd-store.go index 9879e194d..53d266c7d 100644 --- a/cmd/iam-etcd-store.go +++ b/cmd/iam-etcd-store.go @@ -32,7 +32,6 @@ import ( "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/logger" - iampolicy "github.com/minio/pkg/iam/policy" "go.etcd.io/etcd/api/v3/mvccpb" etcd "go.etcd.io/etcd/client/v3" ) @@ -115,7 +114,7 @@ func (ies *IAMEtcdStore) saveIAMConfig(ctx context.Context, item interface{}, it return saveKeyEtcd(ctx, ies.client, itemPath, data, opts...) } -func getIAMConfig(item interface{}, data []byte, itemPath string) error { +func decryptData(data []byte, itemPath string) ([]byte, error) { var err error if !utf8.Valid(data) && GlobalKMS != nil { data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{ @@ -128,10 +127,18 @@ func getIAMConfig(item interface{}, data []byte, itemPath string) error { minioMetaBucket: itemPath, }) if err != nil { - return err + return nil, err } } } + return data, nil +} + +func getIAMConfig(item interface{}, data []byte, itemPath string) error { + data, err := decryptData(data, itemPath) + if err != nil { + return err + } var json = jsoniter.ConfigCompatibleWithStandardLibrary return json.Unmarshal(data, item) } @@ -144,6 +151,14 @@ func (ies *IAMEtcdStore) loadIAMConfig(ctx context.Context, item interface{}, pa return getIAMConfig(item, data, path) } +func (ies *IAMEtcdStore) loadIAMConfigBytes(ctx context.Context, path string) ([]byte, error) { + data, err := readKeyEtcd(ctx, ies.client, path) + if err != nil { + return nil, err + } + return decryptData(data, path) +} + func (ies *IAMEtcdStore) deleteIAMConfig(ctx context.Context, path string) error { return deleteKeyEtcd(ctx, ies.client, path) } @@ -263,34 +278,46 @@ func (ies *IAMEtcdStore) migrateBackendFormat(ctx context.Context) error { return ies.migrateToV1(ctx) } -func (ies *IAMEtcdStore) loadPolicyDoc(ctx context.Context, policy string, m map[string]iampolicy.Policy) error { - var p iampolicy.Policy - err := ies.loadIAMConfig(ctx, &p, getPolicyDocPath(policy)) +func (ies *IAMEtcdStore) loadPolicyDoc(ctx context.Context, policy string, m map[string]PolicyDoc) error { + data, err := ies.loadIAMConfigBytes(ctx, getPolicyDocPath(policy)) if err != nil { if err == errConfigNotFound { return errNoSuchPolicy } return err } + + var p PolicyDoc + err = p.parseJSON(data) + if err != nil { + return err + } + m[policy] = p return nil } -func (ies *IAMEtcdStore) getPolicyDocKV(ctx context.Context, kvs *mvccpb.KeyValue, m map[string]iampolicy.Policy) error { - var p iampolicy.Policy - err := getIAMConfig(&p, kvs.Value, string(kvs.Key)) +func (ies *IAMEtcdStore) getPolicyDocKV(ctx context.Context, kvs *mvccpb.KeyValue, m map[string]PolicyDoc) error { + data, err := decryptData(kvs.Value, string(kvs.Key)) if err != nil { if err == errConfigNotFound { return errNoSuchPolicy } return err } + + var p PolicyDoc + err = p.parseJSON(data) + if err != nil { + return err + } + policy := extractPathPrefixAndSuffix(string(kvs.Key), iamConfigPoliciesPrefix, path.Base(string(kvs.Key))) m[policy] = p return nil } -func (ies *IAMEtcdStore) loadPolicyDocs(ctx context.Context, m map[string]iampolicy.Policy) error { +func (ies *IAMEtcdStore) loadPolicyDocs(ctx context.Context, m map[string]PolicyDoc) error { ctx, cancel := context.WithTimeout(ctx, defaultContextTimeout) defer cancel() // Retrieve all keys and values to avoid too many calls to etcd in case of @@ -473,7 +500,7 @@ func (ies *IAMEtcdStore) loadMappedPolicies(ctx context.Context, userType IAMUse } -func (ies *IAMEtcdStore) savePolicyDoc(ctx context.Context, policyName string, p iampolicy.Policy) error { +func (ies *IAMEtcdStore) savePolicyDoc(ctx context.Context, policyName string, p PolicyDoc) error { return ies.saveIAMConfig(ctx, &p, getPolicyDocPath(policyName)) } diff --git a/cmd/iam-object-store.go b/cmd/iam-object-store.go index 075c2c057..a288254d0 100644 --- a/cmd/iam-object-store.go +++ b/cmd/iam-object-store.go @@ -29,7 +29,6 @@ import ( "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/logger" - iampolicy "github.com/minio/pkg/iam/policy" ) // IAMObjectStore implements IAMStorageAPI @@ -218,19 +217,27 @@ func (iamOS *IAMObjectStore) saveIAMConfig(ctx context.Context, item interface{} return saveConfig(ctx, iamOS.objAPI, objPath, data) } -func (iamOS *IAMObjectStore) loadIAMConfig(ctx context.Context, item interface{}, objPath string) error { - data, err := readConfig(ctx, iamOS.objAPI, objPath) +func (iamOS *IAMObjectStore) loadIAMConfigBytesWithMetadata(ctx context.Context, objPath string) ([]byte, ObjectInfo, error) { + data, meta, err := readConfigWithMetadata(ctx, iamOS.objAPI, objPath) if err != nil { - return err + return nil, meta, err } if !utf8.Valid(data) && GlobalKMS != nil { data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{ minioMetaBucket: path.Join(minioMetaBucket, objPath), }) if err != nil { - return err + return nil, meta, err } } + return data, meta, nil +} + +func (iamOS *IAMObjectStore) loadIAMConfig(ctx context.Context, item interface{}, objPath string) error { + data, _, err := iamOS.loadIAMConfigBytesWithMetadata(ctx, objPath) + if err != nil { + return err + } var json = jsoniter.ConfigCompatibleWithStandardLibrary return json.Unmarshal(data, item) } @@ -239,20 +246,34 @@ func (iamOS *IAMObjectStore) deleteIAMConfig(ctx context.Context, path string) e return deleteConfig(ctx, iamOS.objAPI, path) } -func (iamOS *IAMObjectStore) loadPolicyDoc(ctx context.Context, policy string, m map[string]iampolicy.Policy) error { - var p iampolicy.Policy - err := iamOS.loadIAMConfig(ctx, &p, getPolicyDocPath(policy)) +func (iamOS *IAMObjectStore) loadPolicyDoc(ctx context.Context, policy string, m map[string]PolicyDoc) error { + data, objInfo, err := iamOS.loadIAMConfigBytesWithMetadata(ctx, getPolicyDocPath(policy)) if err != nil { if err == errConfigNotFound { return errNoSuchPolicy } return err } + + var p PolicyDoc + err = p.parseJSON(data) + if err != nil { + return err + } + + if p.Version == 0 { + // This means that policy was in the old version (without any + // timestamp info). We fetch the mod time of the file and save + // that as create and update date. + p.CreateDate = objInfo.ModTime + p.UpdateDate = objInfo.ModTime + } + m[policy] = p return nil } -func (iamOS *IAMObjectStore) loadPolicyDocs(ctx context.Context, m map[string]iampolicy.Policy) error { +func (iamOS *IAMObjectStore) loadPolicyDocs(ctx context.Context, m map[string]PolicyDoc) error { for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPoliciesPrefix) { if item.Err != nil { return item.Err @@ -385,7 +406,7 @@ func (iamOS *IAMObjectStore) loadMappedPolicies(ctx context.Context, userType IA return nil } -func (iamOS *IAMObjectStore) savePolicyDoc(ctx context.Context, policyName string, p iampolicy.Policy) error { +func (iamOS *IAMObjectStore) savePolicyDoc(ctx context.Context, policyName string, p PolicyDoc) error { return iamOS.saveIAMConfig(ctx, &p, getPolicyDocPath(policyName)) } diff --git a/cmd/iam-store.go b/cmd/iam-store.go index 454b64cda..3e2c48809 100644 --- a/cmd/iam-store.go +++ b/cmd/iam-store.go @@ -24,8 +24,10 @@ import ( "errors" "fmt" "strings" + "time" "github.com/dustin/go-humanize" + jsoniter "github.com/json-iterator/go" "github.com/minio/madmin-go" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/auth" @@ -169,6 +171,64 @@ func newMappedPolicy(policy string) MappedPolicy { return MappedPolicy{Version: 1, Policies: policy} } +// PolicyDoc represents an IAM policy with some metadata. +type PolicyDoc struct { + Version int `json:",omitempty"` + Policy iampolicy.Policy + CreateDate time.Time `json:",omitempty"` + UpdateDate time.Time `json:",omitempty"` +} + +func newPolicyDoc(p iampolicy.Policy) PolicyDoc { + now := UTCNow().Round(time.Millisecond) + return PolicyDoc{ + Version: 1, + Policy: p, + CreateDate: now, + UpdateDate: now, + } +} + +// defaultPolicyDoc - used to wrap a default policy as PolicyDoc. +func defaultPolicyDoc(p iampolicy.Policy) PolicyDoc { + return PolicyDoc{ + Version: 1, + Policy: p, + } +} + +func (d *PolicyDoc) update(p iampolicy.Policy) { + now := UTCNow().Round(time.Millisecond) + d.UpdateDate = now + if d.CreateDate.IsZero() { + d.CreateDate = now + } + d.Policy = p +} + +// parseJSON parses both the old and the new format for storing policy +// definitions. +// +// The on-disk format of policy definitions has changed (around early 12/2021) +// from iampolicy.Policy to PolicyDoc. To avoid a migration, loading supports +// both the old and the new formats. +func (d *PolicyDoc) parseJSON(data []byte) error { + var json = jsoniter.ConfigCompatibleWithStandardLibrary + var doc PolicyDoc + err := json.Unmarshal(data, &doc) + if err != nil { + err2 := json.Unmarshal(data, &doc.Policy) + if err2 != nil { + // Just return the first error. + return err + } + d.Policy = doc.Policy + return nil + } + *d = doc + return nil +} + // key options type options struct { ttl int64 // expiry in seconds @@ -182,7 +242,7 @@ type iamWatchEvent struct { // iamCache contains in-memory cache of IAM data. type iamCache struct { // map of policy names to policy definitions - iamPolicyDocsMap map[string]iampolicy.Policy + iamPolicyDocsMap map[string]PolicyDoc // map of usernames to credentials iamUsersMap map[string]auth.Credentials // map of group names to group info @@ -197,7 +257,7 @@ type iamCache struct { func newIamCache() *iamCache { return &iamCache{ - iamPolicyDocsMap: map[string]iampolicy.Policy{}, + iamPolicyDocsMap: map[string]PolicyDoc{}, iamUsersMap: map[string]auth.Credentials{}, iamGroupsMap: map[string]GroupInfo{}, iamUserGroupMemberships: map[string]set.StringSet{}, @@ -332,8 +392,8 @@ type IAMStorageAPI interface { getUsersSysType() UsersSysType - loadPolicyDoc(ctx context.Context, policy string, m map[string]iampolicy.Policy) error - loadPolicyDocs(ctx context.Context, m map[string]iampolicy.Policy) error + loadPolicyDoc(ctx context.Context, policy string, m map[string]PolicyDoc) error + loadPolicyDocs(ctx context.Context, m map[string]PolicyDoc) error loadUser(ctx context.Context, user string, userType IAMUserType, m map[string]auth.Credentials) error loadUsers(ctx context.Context, userType IAMUserType, m map[string]auth.Credentials) error @@ -348,7 +408,7 @@ type IAMStorageAPI interface { loadIAMConfig(ctx context.Context, item interface{}, path string) error deleteIAMConfig(ctx context.Context, path string) error - savePolicyDoc(ctx context.Context, policyName string, p iampolicy.Policy) error + savePolicyDoc(ctx context.Context, policyName string, p PolicyDoc) error saveMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, mp MappedPolicy, opts ...options) error saveUserIdentity(ctx context.Context, name string, userType IAMUserType, u UserIdentity, opts ...options) error saveGroupInfo(ctx context.Context, group string, gi GroupInfo) error @@ -366,10 +426,10 @@ type iamStorageWatcher interface { } // Set default canned policies only if not already overridden by users. -func setDefaultCannedPolicies(policies map[string]iampolicy.Policy) { +func setDefaultCannedPolicies(policies map[string]PolicyDoc) { for _, v := range iampolicy.DefaultPolicies { if _, ok := policies[v.Name]; !ok { - policies[v.Name] = v.Definition + policies[v.Name] = defaultPolicyDoc(v.Definition) } } } @@ -947,13 +1007,31 @@ func (store *IAMStoreSys) GetPolicy(name string) (iampolicy.Policy, error) { } v, ok := cache.iamPolicyDocsMap[policy] if !ok { - return v, errNoSuchPolicy + return v.Policy, errNoSuchPolicy } - combinedPolicy = combinedPolicy.Merge(v) + combinedPolicy = combinedPolicy.Merge(v.Policy) } return combinedPolicy, nil } +// GetPolicyDoc - gets the policy doc which has the policy and some metadata. +// Exactly one policy must be specified here. +func (store *IAMStoreSys) GetPolicyDoc(name string) (r PolicyDoc, err error) { + name = strings.TrimSpace(name) + if name == "" { + return r, errInvalidArgument + } + + cache := store.rlock() + defer store.runlock() + + v, ok := cache.iamPolicyDocsMap[name] + if !ok { + return r, errNoSuchPolicy + } + return v, nil +} + // SetPolicy - creates a policy with name. func (store *IAMStoreSys) SetPolicy(ctx context.Context, name string, policy iampolicy.Policy) error { @@ -964,11 +1042,21 @@ func (store *IAMStoreSys) SetPolicy(ctx context.Context, name string, policy iam cache := store.lock() defer store.unlock() - if err := store.savePolicyDoc(ctx, name, policy); err != nil { + var ( + d PolicyDoc + ok bool + ) + if d, ok = cache.iamPolicyDocsMap[name]; ok { + d.update(policy) + } else { + d = newPolicyDoc(policy) + } + + if err := store.savePolicyDoc(ctx, name, d); err != nil { return err } - cache.iamPolicyDocsMap[name] = policy + cache.iamPolicyDocsMap[name] = d return nil } @@ -979,7 +1067,7 @@ func (store *IAMStoreSys) ListPolicies(ctx context.Context, bucketName string) ( cache := store.lock() defer store.unlock() - m := map[string]iampolicy.Policy{} + m := map[string]PolicyDoc{} err := store.loadPolicyDocs(ctx, m) if err != nil { return nil, err @@ -992,8 +1080,8 @@ func (store *IAMStoreSys) ListPolicies(ctx context.Context, bucketName string) ( ret := map[string]iampolicy.Policy{} for k, v := range m { - if bucketName == "" || v.MatchResource(bucketName) { - ret[k] = v + if bucketName == "" || v.Policy.MatchResource(bucketName) { + ret[k] = v.Policy } } @@ -1011,9 +1099,9 @@ func filterPolicies(cache *iamCache, policyName string, bucketName string) (stri } p, found := cache.iamPolicyDocsMap[policy] if found { - if bucketName == "" || p.MatchResource(bucketName) { + if bucketName == "" || p.Policy.MatchResource(bucketName) { policies = append(policies, policy) - combinedPolicy = combinedPolicy.Merge(p) + combinedPolicy = combinedPolicy.Merge(p.Policy) } } } diff --git a/cmd/iam.go b/cmd/iam.go index b7ddd892a..33572aa96 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -477,13 +477,28 @@ func (sys *IAMSys) DeletePolicy(ctx context.Context, policyName string, notifyPe return nil } -// InfoPolicy - expands the canned policy into its JSON structure. -func (sys *IAMSys) InfoPolicy(policyName string) (iampolicy.Policy, error) { +// InfoPolicy - returns the policy definition with some metadata. +func (sys *IAMSys) InfoPolicy(policyName string) (*madmin.PolicyInfo, error) { if !sys.Initialized() { - return iampolicy.Policy{}, errServerNotInitialized + return nil, errServerNotInitialized } - return sys.store.GetPolicy(policyName) + d, err := sys.store.GetPolicyDoc(policyName) + if err != nil { + return nil, err + } + + pdata, err := json.Marshal(d.Policy) + if err != nil { + return nil, err + } + + return &madmin.PolicyInfo{ + PolicyName: policyName, + Policy: pdata, + CreateDate: d.CreateDate, + UpdateDate: d.UpdateDate, + }, nil } // ListPolicies - lists all canned policies. diff --git a/cmd/typed-errors.go b/cmd/typed-errors.go index 8565431ea..31c5ad978 100644 --- a/cmd/typed-errors.go +++ b/cmd/typed-errors.go @@ -84,6 +84,10 @@ var errNoSuchPolicy = errors.New("Specified canned policy does not exist") // error returned when policy to be deleted is in use. var errPolicyInUse = errors.New("Specified policy is in use and cannot be deleted.") +// error returned when more than a single policy is specified when only one is +// expectd. +var errTooManyPolicies = errors.New("Only a single policy may be specified here.") + // error returned in IAM subsystem when an external users systems is configured. var errIAMActionNotAllowed = errors.New("Specified IAM action is not allowed")