Add option to policy info API to return create/mod timestamps (#13796)

- This introduces a new admin API with a query parameter (v=2) to return a
response with the timestamps

- Older API still works for compatibility/smooth transition in console
This commit is contained in:
Aditya Manthramurthy 2021-12-11 09:03:39 -08:00 committed by GitHub
parent 878d368cea
commit 44fefe5b9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 245 additions and 53 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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
}

View file

@ -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))
}

View file

@ -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))
}

View file

@ -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)
}
}
}

View file

@ -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.

View file

@ -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")