github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/config/policy/plugin/config.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package plugin
    19  
    20  import (
    21  	"bytes"
    22  	"encoding/json"
    23  	"io"
    24  	"net/http"
    25  	"time"
    26  
    27  	"github.com/minio/minio/internal/config"
    28  	xhttp "github.com/minio/minio/internal/http"
    29  	xnet "github.com/minio/pkg/v2/net"
    30  	"github.com/minio/pkg/v2/policy"
    31  )
    32  
    33  // Authorization Plugin config and env variables
    34  const (
    35  	URL         = "url"
    36  	AuthToken   = "auth_token"
    37  	EnableHTTP2 = "enable_http2"
    38  
    39  	EnvPolicyPluginURL         = "MINIO_POLICY_PLUGIN_URL"
    40  	EnvPolicyPluginAuthToken   = "MINIO_POLICY_PLUGIN_AUTH_TOKEN"
    41  	EnvPolicyPluginEnableHTTP2 = "MINIO_POLICY_PLUGIN_ENABLE_HTTP2"
    42  )
    43  
    44  // DefaultKVS - default config for Authz plugin config
    45  var (
    46  	DefaultKVS = config.KVS{
    47  		config.KV{
    48  			Key:   URL,
    49  			Value: "",
    50  		},
    51  		config.KV{
    52  			Key:   AuthToken,
    53  			Value: "",
    54  		},
    55  		config.KV{
    56  			Key:   EnableHTTP2,
    57  			Value: "off",
    58  		},
    59  	}
    60  )
    61  
    62  // Args for general purpose policy engine configuration.
    63  type Args struct {
    64  	URL         *xnet.URL             `json:"url"`
    65  	AuthToken   string                `json:"authToken"`
    66  	Transport   http.RoundTripper     `json:"-"`
    67  	CloseRespFn func(r io.ReadCloser) `json:"-"`
    68  }
    69  
    70  // Validate - validate opa configuration params.
    71  func (a *Args) Validate() error {
    72  	req, err := http.NewRequest(http.MethodPost, a.URL.String(), bytes.NewReader([]byte("")))
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	req.Header.Set("Content-Type", "application/json")
    78  	if a.AuthToken != "" {
    79  		req.Header.Set("Authorization", a.AuthToken)
    80  	}
    81  
    82  	client := &http.Client{Transport: a.Transport}
    83  	resp, err := client.Do(req)
    84  	if err != nil {
    85  		return err
    86  	}
    87  	defer a.CloseRespFn(resp.Body)
    88  
    89  	return nil
    90  }
    91  
    92  // UnmarshalJSON - decodes JSON data.
    93  func (a *Args) UnmarshalJSON(data []byte) error {
    94  	// subtype to avoid recursive call to UnmarshalJSON()
    95  	type subArgs Args
    96  	var so subArgs
    97  
    98  	if err := json.Unmarshal(data, &so); err != nil {
    99  		return err
   100  	}
   101  
   102  	oa := Args(so)
   103  	if oa.URL == nil || oa.URL.String() == "" {
   104  		*a = oa
   105  		return nil
   106  	}
   107  
   108  	*a = oa
   109  	return nil
   110  }
   111  
   112  // AuthZPlugin - implements opa policy agent calls.
   113  type AuthZPlugin struct {
   114  	args   Args
   115  	client *http.Client
   116  }
   117  
   118  // Enabled returns if AuthZPlugin is enabled.
   119  func Enabled(kvs config.KVS) bool {
   120  	return kvs.Get(URL) != ""
   121  }
   122  
   123  // LookupConfig lookup AuthZPlugin from config, override with any ENVs.
   124  func LookupConfig(s config.Config, httpSettings xhttp.ConnSettings, closeRespFn func(io.ReadCloser)) (Args, error) {
   125  	args := Args{}
   126  
   127  	if err := s.CheckValidKeys(config.PolicyPluginSubSys, nil); err != nil {
   128  		return args, err
   129  	}
   130  
   131  	getCfg := func(cfgParam string) string {
   132  		// As parameters are already validated, we skip checking
   133  		// if the config param was found.
   134  		val, _, _ := s.ResolveConfigParam(config.PolicyPluginSubSys, config.Default, cfgParam, false)
   135  		return val
   136  	}
   137  
   138  	pluginURL := getCfg(URL)
   139  	if pluginURL == "" {
   140  		return args, nil
   141  	}
   142  
   143  	u, err := xnet.ParseHTTPURL(pluginURL)
   144  	if err != nil {
   145  		return args, err
   146  	}
   147  
   148  	enableHTTP2 := false
   149  	if v := getCfg(EnableHTTP2); v != "" {
   150  		enableHTTP2, err = config.ParseBool(v)
   151  		if err != nil {
   152  			return args, err
   153  		}
   154  	}
   155  	httpSettings.EnableHTTP2 = enableHTTP2
   156  	transport := httpSettings.NewHTTPTransportWithTimeout(time.Minute)
   157  
   158  	args = Args{
   159  		URL:         u,
   160  		AuthToken:   getCfg(AuthToken),
   161  		Transport:   transport,
   162  		CloseRespFn: closeRespFn,
   163  	}
   164  	if err = args.Validate(); err != nil {
   165  		return args, err
   166  	}
   167  	return args, nil
   168  }
   169  
   170  // New - initializes Authorization Management Plugin.
   171  func New(args Args) *AuthZPlugin {
   172  	if args.URL == nil || args.URL.Scheme == "" && args.AuthToken == "" {
   173  		return nil
   174  	}
   175  	return &AuthZPlugin{
   176  		args:   args,
   177  		client: &http.Client{Transport: args.Transport},
   178  	}
   179  }
   180  
   181  // IsAllowed - checks given policy args is allowed to continue the REST API.
   182  func (o *AuthZPlugin) IsAllowed(args policy.Args) (bool, error) {
   183  	if o == nil {
   184  		return false, nil
   185  	}
   186  
   187  	// Access Management Plugin Input
   188  	body := make(map[string]interface{})
   189  	body["input"] = args
   190  
   191  	inputBytes, err := json.Marshal(body)
   192  	if err != nil {
   193  		return false, err
   194  	}
   195  
   196  	req, err := http.NewRequest(http.MethodPost, o.args.URL.String(), bytes.NewReader(inputBytes))
   197  	if err != nil {
   198  		return false, err
   199  	}
   200  
   201  	req.Header.Set("Content-Type", "application/json")
   202  	if o.args.AuthToken != "" {
   203  		req.Header.Set("Authorization", o.args.AuthToken)
   204  	}
   205  
   206  	resp, err := o.client.Do(req)
   207  	if err != nil {
   208  		return false, err
   209  	}
   210  	defer o.args.CloseRespFn(resp.Body)
   211  
   212  	// Read the body to be saved later.
   213  	opaRespBytes, err := io.ReadAll(resp.Body)
   214  	if err != nil {
   215  		return false, err
   216  	}
   217  
   218  	// Handle large OPA responses when OPA URL is of
   219  	// form http://localhost:8181/v1/data/httpapi/authz
   220  	type opaResultAllow struct {
   221  		Result struct {
   222  			Allow bool `json:"allow"`
   223  		} `json:"result"`
   224  	}
   225  
   226  	// Handle simpler OPA responses when OPA URL is of
   227  	// form http://localhost:8181/v1/data/httpapi/authz/allow
   228  	type opaResult struct {
   229  		Result bool `json:"result"`
   230  	}
   231  
   232  	respBody := bytes.NewReader(opaRespBytes)
   233  
   234  	var result opaResult
   235  	if err = json.NewDecoder(respBody).Decode(&result); err != nil {
   236  		respBody.Seek(0, 0)
   237  		var resultAllow opaResultAllow
   238  		if err = json.NewDecoder(respBody).Decode(&resultAllow); err != nil {
   239  			return false, err
   240  		}
   241  		return resultAllow.Result.Allow, nil
   242  	}
   243  
   244  	return result.Result, nil
   245  }