From 83071a345997e8f8a3100df60e414de79da84007 Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Tue, 10 May 2022 17:14:55 -0700 Subject: [PATCH] Add support for Access Management Plugin (#14875) - This change renames the OPA integration as Access Management Plugin - there is nothing specific to OPA in the integration, it is just a webhook. - OPA configuration is automatically migrated to Access Management Plugin and OPA specific configuration is marked as deprecated. - OPA doc is updated and moved. --- cmd/admin-handlers-config-kv.go | 6 +- cmd/auth-handler.go | 4 +- cmd/config-current.go | 42 ++++- cmd/globals.go | 6 +- cmd/iam.go | 6 +- cmd/sts-handlers.go | 4 +- docs/iam/access-management-plugin.md | 159 +++++++++++++++++ docs/iam/access-manager-plugin.go | 83 +++++++++ docs/{sts => iam}/opa.md | 9 +- internal/config/config.go | 3 + internal/config/policy/opa/help.go | 4 +- internal/config/policy/plugin/config.go | 223 ++++++++++++++++++++++++ internal/config/policy/plugin/help.go | 49 ++++++ 13 files changed, 572 insertions(+), 26 deletions(-) create mode 100644 docs/iam/access-management-plugin.md create mode 100644 docs/iam/access-manager-plugin.go rename docs/{sts => iam}/opa.md (84%) create mode 100644 internal/config/policy/plugin/config.go create mode 100644 internal/config/policy/plugin/help.go diff --git a/cmd/admin-handlers-config-kv.go b/cmd/admin-handlers-config-kv.go index f031e6afe..fb4c683f0 100644 --- a/cmd/admin-handlers-config-kv.go +++ b/cmd/admin-handlers-config-kv.go @@ -33,7 +33,7 @@ import ( "github.com/minio/minio/internal/config/etcd" xldap "github.com/minio/minio/internal/config/identity/ldap" "github.com/minio/minio/internal/config/identity/openid" - "github.com/minio/minio/internal/config/policy/opa" + polplugin "github.com/minio/minio/internal/config/policy/plugin" "github.com/minio/minio/internal/config/storageclass" "github.com/minio/minio/internal/logger" iampolicy "github.com/minio/pkg/iam/policy" @@ -436,8 +436,8 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques off = !cache.Enabled(kv) case config.StorageClassSubSys: off = !storageclass.Enabled(kv) - case config.PolicyOPASubSys: - off = !opa.Enabled(kv) + case config.PolicyPluginSubSys: + off = !polplugin.Enabled(kv) case config.IdentityOpenIDSubSys: off = !openid.Enabled(kv) case config.IdentityLDAPSubSys: diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go index 5fa945a31..28320724a 100644 --- a/cmd/auth-handler.go +++ b/cmd/auth-handler.go @@ -217,8 +217,8 @@ func getClaimsFromTokenWithSecret(token, secret string) (map[string]interface{}, } } - // If OPA is set, return without any further checks. - if globalPolicyOPA != nil { + // If AuthZPlugin is set, return without any further checks. + if globalAuthZPlugin != nil { return claims.Map(), nil } diff --git a/cmd/config-current.go b/cmd/config-current.go index 01ff7df96..99d2cd6fd 100644 --- a/cmd/config-current.go +++ b/cmd/config-current.go @@ -37,6 +37,7 @@ import ( xtls "github.com/minio/minio/internal/config/identity/tls" "github.com/minio/minio/internal/config/notify" "github.com/minio/minio/internal/config/policy/opa" + polplugin "github.com/minio/minio/internal/config/policy/plugin" "github.com/minio/minio/internal/config/scanner" "github.com/minio/minio/internal/config/storageclass" "github.com/minio/minio/internal/config/subnet" @@ -56,6 +57,7 @@ func initHelp() { config.IdentityOpenIDSubSys: openid.DefaultKVS, config.IdentityTLSSubSys: xtls.DefaultKVS, config.PolicyOPASubSys: opa.DefaultKVS, + config.PolicyPluginSubSys: polplugin.DefaultKVS, config.SiteSubSys: config.DefaultSiteKVS, config.RegionSubSys: config.DefaultRegionKVS, config.APISubSys: api.DefaultKVS, @@ -107,8 +109,8 @@ func initHelp() { Description: "enable X.509 TLS certificate SSO support", }, config.HelpKV{ - Key: config.PolicyOPASubSys, - Description: "enable external OPA for policy enforcement", + Key: config.PolicyPluginSubSys, + Description: "enable Access Management Plugin for policy enforcement", }, config.HelpKV{ Key: config.APISubSys, @@ -219,6 +221,7 @@ func initHelp() { config.IdentityLDAPSubSys: xldap.Help, config.IdentityTLSSubSys: xtls.Help, config.PolicyOPASubSys: opa.Help, + config.PolicyPluginSubSys: polplugin.Help, config.LoggerWebhookSubSys: logger.Help, config.AuditWebhookSubSys: logger.HelpWebhook, config.AuditKafkaSubSys: logger.HelpKafka, @@ -243,6 +246,10 @@ func initHelp() { Key: config.RegionSubSys, Description: "[DEPRECATED - use `site` instead] label the location of the server", }, + config.PolicyOPASubSys: { + Key: config.PolicyOPASubSys, + Description: "[DEPRECATED - use `policy_plugin` instead] enable external OPA for policy enforcement", + }, } config.RegisterHelpDeprecatedSubSys(deprecatedHelpKVMap) @@ -340,9 +347,20 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er return err } case config.PolicyOPASubSys: - if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default], + // In case legacy OPA config is being set, we treat it as if the + // AuthZPlugin is being set. + subSys = config.PolicyPluginSubSys + fallthrough + case config.PolicyPluginSubSys: + if ppargs, err := polplugin.LookupConfig(s[config.PolicyPluginSubSys][config.Default], NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil { return err + } else if ppargs.URL == nil { + // Check if legacy opa is configured. + if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default], + NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil { + return err + } } default: if config.LoggerSubSystems.Contains(subSys) { @@ -523,12 +541,24 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) { logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err)) } - opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default], + authZPluginCfg, err := polplugin.LookupConfig(s[config.PolicyPluginSubSys][config.Default], NewGatewayHTTPTransport(), xhttp.DrainBody) if err != nil { - logger.LogIf(ctx, fmt.Errorf("Unable to initialize OPA: %w", err)) + logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthZPlugin: %w", err)) } - globalPolicyOPA = opa.New(opaCfg) + if authZPluginCfg.URL == nil { + opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default], + NewGatewayHTTPTransport(), xhttp.DrainBody) + if err != nil { + logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthZPlugin from legacy OPA config: %w", err)) + } else { + authZPluginCfg.URL = opaCfg.URL + authZPluginCfg.AuthToken = opaCfg.AuthToken + authZPluginCfg.Transport = opaCfg.Transport + authZPluginCfg.CloseRespFn = opaCfg.CloseRespFn + } + } + globalAuthZPlugin = polplugin.New(authZPluginCfg) globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default], globalRootCAs) diff --git a/cmd/globals.go b/cmd/globals.go index 3f49f650f..66062644a 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -42,7 +42,7 @@ import ( xldap "github.com/minio/minio/internal/config/identity/ldap" "github.com/minio/minio/internal/config/identity/openid" xtls "github.com/minio/minio/internal/config/identity/tls" - "github.com/minio/minio/internal/config/policy/opa" + polplugin "github.com/minio/minio/internal/config/policy/plugin" "github.com/minio/minio/internal/config/storageclass" "github.com/minio/minio/internal/config/subnet" xhttp "github.com/minio/minio/internal/http" @@ -290,8 +290,8 @@ var ( // Some standard content-types which we strictly dis-allow for compression. standardExcludeCompressContentTypes = []string{"video/*", "audio/*", "application/zip", "application/x-gzip", "application/x-zip-compressed", " application/x-compress", "application/x-spoon"} - // OPA policy system. - globalPolicyOPA *opa.Opa + // AuthZ Plugin system. + globalAuthZPlugin *polplugin.AuthZPlugin // Deployment ID - unique per deployment globalDeploymentID string diff --git a/cmd/iam.go b/cmd/iam.go index 791380aad..4fbd139c7 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -681,7 +681,7 @@ func (sys *IAMSys) SetTempUser(ctx context.Context, accessKey string, cred auth. return errServerNotInitialized } - if globalPolicyOPA != nil { + if globalAuthZPlugin != nil { // If OPA is set, we do not need to set a policy mapping. policyName = "" } @@ -1693,8 +1693,8 @@ func (sys *IAMSys) GetCombinedPolicy(policies ...string) iampolicy.Policy { // IsAllowed - checks given policy args is allowed to continue the Rest API. func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool { // If opa is configured, use OPA always. - if globalPolicyOPA != nil { - ok, err := globalPolicyOPA.IsAllowed(args) + if globalAuthZPlugin != nil { + ok, err := globalAuthZPlugin.IsAllowed(args) if err != nil { logger.LogIf(GlobalContext, err) } diff --git a/cmd/sts-handlers.go b/cmd/sts-handlers.go index 5bfc22eb6..042365fe0 100644 --- a/cmd/sts-handlers.go +++ b/cmd/sts-handlers.go @@ -383,7 +383,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithSSO(w http.ResponseWriter, r *http.Requ policyName = globalIAMSys.CurrentPolicies(policies) } - if globalPolicyOPA == nil { + if globalAuthZPlugin == nil { if !ok { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID())) @@ -593,7 +593,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r * // Check if this user or their groups have a policy applied. ldapPolicies, _ := globalIAMSys.PolicyDBGet(ldapUserDN, false, groupDistNames...) - if len(ldapPolicies) == 0 && globalPolicyOPA == nil { + if len(ldapPolicies) == 0 && globalAuthZPlugin == nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("expecting a policy to be set for user `%s` or one of their groups: `%s` - rejecting this request", ldapUserDN, strings.Join(groupDistNames, "`,`"))) diff --git a/docs/iam/access-management-plugin.md b/docs/iam/access-management-plugin.md new file mode 100644 index 000000000..61c3f1282 --- /dev/null +++ b/docs/iam/access-management-plugin.md @@ -0,0 +1,159 @@ +# Access Management Plugin Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) + +MinIO now includes support for using an Access Management Plugin. This is to allow object storage access control to be managed externally via a webhook. + +When configured, MinIO sends request and credential details for every API call to an external HTTP(S) endpoint and expects an allow/deny response. MinIO is thus able to delegate access management to an external system, and users are able to use a custom solution instead of S3 standard IAM policies. + +Latency sensitive applications may notice an increased latency due to a request to the external plugin upon every authenticated request to MinIO. User are advised to provision their infrastructure such that latency and performance is acceptable. + +## Quickstart + +To easily try out the feature, run the included demo Access Management Plugin program in this directory: + +```sh +go run access-manager-plugin.go +``` + +This program, lets the admin user perform any action and prevents all other users from performing `s3:Put*` operations. + +In another terminal start MinIO: + +```sh +export MINIO_CI_CD=1 +export MINIO_ROOT_USER=minio +export MINIO_ROOT_PASSWORD=minio123 +export MINIO_POLICY_OPA_URL=http://localhost:8080/ +minio server /tmp/disk{1...4} +``` + +Now, let's test it out with `mc`: + +```sh +mc alias set myminio http://localhost:9000 minio minio123 +mc ls myminio +mc mb myminio/test +mc cp /etc/issue myminio/test +mc admin user add myminio foo foobar123 +export MC_HOST_foo=http://foo:foobar123@localhost:9000 +mc ls foo +mc cp /etc/issue myminio/test/issue2 +``` + +Only the last operation would fail with a permissions error. + +## Configuration + +Access Management Plugin can be configured with environment variables: + +```sh +$ mc admin config set dminio1 policy_plugin --env +KEY: +policy_plugin enable Access Management Plugin for policy enforcement + +ARGS: +MINIO_POLICY_PLUGIN_URL* (url) plugin hook endpoint (HTTP(S)) e.g. "http://localhost:8181/v1/data/httpapi/authz/allow" +MINIO_POLICY_PLUGIN_AUTH_TOKEN (string) authorization token for plugin hook endpoint +MINIO_POLICY_PLUGIN_COMMENT (sentence) optionally add a comment to this setting +``` + +## Request and Response + +MinIO will make a `POST` request with a JSON body to the given plugin URL. If the auth token parameter is set, it will be sent as an authorization header. + +The JSON body structure can be seen from this sample: + +
Request Body Sample + +```json +{ + "input": { + "account": "minio", + "groups": null, + "action": "s3:ListBucket", + "bucket": "test", + "conditions": { + "Authorization": [ + "AWS4-HMAC-SHA256 Credential=minio/20220507/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=62012db6c47d697620cf6c68f0f45f6e34894589a53ab1faf6dc94338468c78a" + ], + "CurrentTime": [ + "2022-05-07T18:31:41Z" + ], + "Delimiter": [ + "/" + ], + "EpochTime": [ + "1651948301" + ], + "Prefix": [ + "" + ], + "Referer": [ + "" + ], + "SecureTransport": [ + "false" + ], + "SourceIp": [ + "127.0.0.1" + ], + "User-Agent": [ + "MinIO (linux; amd64) minio-go/v7.0.24 mc/DEVELOPMENT.2022-04-20T23-07-53Z" + ], + "UserAgent": [ + "MinIO (linux; amd64) minio-go/v7.0.24 mc/DEVELOPMENT.2022-04-20T23-07-53Z" + ], + "X-Amz-Content-Sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "X-Amz-Date": [ + "20220507T183141Z" + ], + "authType": [ + "REST-HEADER" + ], + "principaltype": [ + "Account" + ], + "signatureversion": [ + "AWS4-HMAC-SHA256" + ], + "userid": [ + "minio" + ], + "username": [ + "minio" + ], + "versionid": [ + "" + ] + }, + "owner": true, + "object": "", + "claims": {}, + "denyOnly": false + } +} +``` + +
+ +The response expected by MinIO, is a JSON body with a boolean: + +```json +{ + "result": true +} +``` + +The following structure is also accepted: + +```json +{ + "result": { + "allow": true + } +} +``` + +Any unmentioned JSON object keys in the above are ignored. + diff --git a/docs/iam/access-manager-plugin.go b/docs/iam/access-manager-plugin.go new file mode 100644 index 000000000..118daee3d --- /dev/null +++ b/docs/iam/access-manager-plugin.go @@ -0,0 +1,83 @@ +//go:build ignore +// +build ignore + +// Copyright (c) 2015-2022 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" +) + +func writeErrorResponse(w http.ResponseWriter, err error) { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{ + "error": fmt.Sprintf("%v", err), + }) +} + +type Result struct { + Result bool `json:"result"` +} + +func mainHandler(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + writeErrorResponse(w, err) + return + } + + reqMap := make(map[string]interface{}) + err = json.Unmarshal(body, &reqMap) + if err != nil { + writeErrorResponse(w, err) + return + } + + // fmt.Printf("request: %#v\n", reqMap) + + m := reqMap["input"].(map[string]interface{}) + accountValue := m["account"].(string) + actionValue := m["action"].(string) + + // Allow user `minio` to perform any action. + var res Result + if accountValue == "minio" { + res.Result = true + } else { + // All other users may not perform any `s3:Put*` operations. + res.Result = true + if strings.HasPrefix(actionValue, "s3:Put") { + res.Result = false + } + } + fmt.Printf("account: %v | action: %v | allowed: %v\n", accountValue, actionValue, res.Result) + json.NewEncoder(w).Encode(res) + return +} + +func main() { + http.HandleFunc("/", mainHandler) + + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/docs/sts/opa.md b/docs/iam/opa.md similarity index 84% rename from docs/sts/opa.md rename to docs/iam/opa.md index 64ab6f0a2..cc4a2261d 100644 --- a/docs/sts/opa.md +++ b/docs/iam/opa.md @@ -1,6 +1,8 @@ # OPA Quickstart Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) OPA is a lightweight general-purpose policy engine that can be co-located with MinIO server, in this document we talk about how to use OPA HTTP API to authorize requests. It can be used with any type of credentials (STS based like OpenID or LDAP, regular IAM users or service accounts). +OPA is enabled through MinIO's Access Management Plugin feature. + ## Get started ### 1. Start OPA in a container @@ -48,10 +50,10 @@ curl -X PUT --data-binary @example.rego \ ### 4. Setup MinIO with OPA -Set the `MINIO_POLICY_OPA_URL` as the endpoint that MinIO should send authorization requests to. Then start the server. +Set the `MINIO_POLICY_PLUGIN_URL` as the endpoint that MinIO should send authorization requests to. Then start the server. ```sh -export MINIO_POLICY_OPA_URL=http://localhost:8181/v1/data/httpapi/authz/allow +export MINIO_POLICY_PLUGIN_URL=http://localhost:8181/v1/data/httpapi/authz/allow export MINIO_CI_CD=1 export MINIO_ROOT_USER=minio export MINIO_ROOT_PASSWORD=minio123 @@ -77,6 +79,3 @@ mc cat foo/test/issue mc cp /etc/issue myminio/test/issue2 ``` -## Explore Further -- [MinIO STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide) -- [The MinIO documentation website](https://docs.minio.io) diff --git a/internal/config/config.go b/internal/config/config.go index 8ad87279c..307f18606 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -69,6 +69,7 @@ const ( const ( CredentialsSubSys = "credentials" PolicyOPASubSys = "policy_opa" + PolicyPluginSubSys = "policy_plugin" IdentityOpenIDSubSys = "identity_openid" IdentityLDAPSubSys = "identity_ldap" IdentityTLSSubSys = "identity_tls" @@ -141,6 +142,7 @@ var SubSystems = set.CreateStringSet( AuditWebhookSubSys, AuditKafkaSubSys, PolicyOPASubSys, + PolicyPluginSubSys, IdentityLDAPSubSys, IdentityOpenIDSubSys, IdentityTLSSubSys, @@ -183,6 +185,7 @@ var SubSystemsSingleTargets = set.CreateStringSet([]string{ StorageClassSubSys, CompressionSubSys, PolicyOPASubSys, + PolicyPluginSubSys, IdentityLDAPSubSys, IdentityTLSSubSys, HealSubSys, diff --git a/internal/config/policy/opa/help.go b/internal/config/policy/opa/help.go index 9d1f343b7..4865406e3 100644 --- a/internal/config/policy/opa/help.go +++ b/internal/config/policy/opa/help.go @@ -28,13 +28,13 @@ var ( Help = config.HelpKVS{ config.HelpKV{ Key: URL, - Description: `OPA HTTP(s) endpoint e.g. "http://localhost:8181/v1/data/httpapi/authz/allow"` + defaultHelpPostfix(URL), + Description: `[DEPRECATED] OPA HTTP(s) endpoint e.g. "http://localhost:8181/v1/data/httpapi/authz/allow"` + defaultHelpPostfix(URL), Type: "url", Sensitive: true, }, config.HelpKV{ Key: AuthToken, - Description: "authorization token for OPA endpoint" + defaultHelpPostfix(AuthToken), + Description: "[DEPRECATED] authorization token for OPA endpoint" + defaultHelpPostfix(AuthToken), Optional: true, Type: "string", Sensitive: true, diff --git a/internal/config/policy/plugin/config.go b/internal/config/policy/plugin/config.go new file mode 100644 index 000000000..10cfd9951 --- /dev/null +++ b/internal/config/policy/plugin/config.go @@ -0,0 +1,223 @@ +// Copyright (c) 2015-2022 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package plugin + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + + "github.com/minio/minio/internal/config" + "github.com/minio/pkg/env" + iampolicy "github.com/minio/pkg/iam/policy" + xnet "github.com/minio/pkg/net" +) + +// Authorization Plugin config and env variables +const ( + URL = "url" + AuthToken = "auth_token" + + EnvPolicyPluginURL = "MINIO_POLICY_PLUGIN_URL" + EnvPolicyPluginAuthToken = "MINIO_POLICY_PLUGIN_AUTH_TOKEN" +) + +// DefaultKVS - default config for Authz plugin config +var ( + DefaultKVS = config.KVS{ + config.KV{ + Key: URL, + Value: "", + }, + config.KV{ + Key: AuthToken, + Value: "", + }, + } +) + +// Args opa general purpose policy engine configuration. +type Args struct { + URL *xnet.URL `json:"url"` + AuthToken string `json:"authToken"` + Transport http.RoundTripper `json:"-"` + CloseRespFn func(r io.ReadCloser) `json:"-"` +} + +// Validate - validate opa configuration params. +func (a *Args) Validate() error { + req, err := http.NewRequest(http.MethodPost, a.URL.String(), bytes.NewReader([]byte(""))) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + if a.AuthToken != "" { + req.Header.Set("Authorization", a.AuthToken) + } + + client := &http.Client{Transport: a.Transport} + resp, err := client.Do(req) + if err != nil { + return err + } + defer a.CloseRespFn(resp.Body) + + return nil +} + +// UnmarshalJSON - decodes JSON data. +func (a *Args) UnmarshalJSON(data []byte) error { + // subtype to avoid recursive call to UnmarshalJSON() + type subArgs Args + var so subArgs + + if err := json.Unmarshal(data, &so); err != nil { + return err + } + + oa := Args(so) + if oa.URL == nil || oa.URL.String() == "" { + *a = oa + return nil + } + + *a = oa + return nil +} + +// AuthZPlugin - implements opa policy agent calls. +type AuthZPlugin struct { + args Args + client *http.Client +} + +// Enabled returns if AuthZPlugin is enabled. +func Enabled(kvs config.KVS) bool { + return kvs.Get(URL) != "" +} + +// LookupConfig lookup AuthZPlugin from config, override with any ENVs. +func LookupConfig(kv config.KVS, transport *http.Transport, closeRespFn func(io.ReadCloser)) (Args, error) { + args := Args{} + + if err := config.CheckValidKeys(config.PolicyPluginSubSys, kv, DefaultKVS); err != nil { + return args, err + } + + pluginURL := env.Get(EnvPolicyPluginURL, kv.Get(URL)) + if pluginURL == "" { + return args, nil + } + + authToken := env.Get(EnvPolicyPluginAuthToken, kv.Get(AuthToken)) + + u, err := xnet.ParseHTTPURL(pluginURL) + if err != nil { + return args, err + } + args = Args{ + URL: u, + AuthToken: authToken, + Transport: transport, + CloseRespFn: closeRespFn, + } + if err = args.Validate(); err != nil { + return args, err + } + return args, nil +} + +// New - initializes Authorization Management Plugin. +func New(args Args) *AuthZPlugin { + if args.URL == nil || args.URL.Scheme == "" && args.AuthToken == "" { + return nil + } + return &AuthZPlugin{ + args: args, + client: &http.Client{Transport: args.Transport}, + } +} + +// IsAllowed - checks given policy args is allowed to continue the REST API. +func (o *AuthZPlugin) IsAllowed(args iampolicy.Args) (bool, error) { + if o == nil { + return false, nil + } + + // Access Management Plugin Input + body := make(map[string]interface{}) + body["input"] = args + + inputBytes, err := json.Marshal(body) + if err != nil { + return false, err + } + + req, err := http.NewRequest(http.MethodPost, o.args.URL.String(), bytes.NewReader(inputBytes)) + if err != nil { + return false, err + } + + req.Header.Set("Content-Type", "application/json") + if o.args.AuthToken != "" { + req.Header.Set("Authorization", o.args.AuthToken) + } + + resp, err := o.client.Do(req) + if err != nil { + return false, err + } + defer o.args.CloseRespFn(resp.Body) + + // Read the body to be saved later. + opaRespBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false, err + } + + // Handle large OPA responses when OPA URL is of + // form http://localhost:8181/v1/data/httpapi/authz + type opaResultAllow struct { + Result struct { + Allow bool `json:"allow"` + } `json:"result"` + } + + // Handle simpler OPA responses when OPA URL is of + // form http://localhost:8181/v1/data/httpapi/authz/allow + type opaResult struct { + Result bool `json:"result"` + } + + respBody := bytes.NewReader(opaRespBytes) + + var result opaResult + if err = json.NewDecoder(respBody).Decode(&result); err != nil { + respBody.Seek(0, 0) + var resultAllow opaResultAllow + if err = json.NewDecoder(respBody).Decode(&resultAllow); err != nil { + return false, err + } + return resultAllow.Result.Allow, nil + } + + return result.Result, nil +} diff --git a/internal/config/policy/plugin/help.go b/internal/config/policy/plugin/help.go new file mode 100644 index 000000000..1f27e30be --- /dev/null +++ b/internal/config/policy/plugin/help.go @@ -0,0 +1,49 @@ +// Copyright (c) 2015-2022 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package plugin + +import "github.com/minio/minio/internal/config" + +// Help template for Access Management Plugin policy feature. +var ( + defaultHelpPostfix = func(key string) string { + return config.DefaultHelpPostfix(DefaultKVS, key) + } + + Help = config.HelpKVS{ + config.HelpKV{ + Key: URL, + Description: `plugin hook endpoint (HTTP(S)) e.g. "http://localhost:8181/v1/data/httpapi/authz/allow"` + defaultHelpPostfix(URL), + Type: "url", + Sensitive: true, + }, + config.HelpKV{ + Key: AuthToken, + Description: "authorization token for plugin hook endpoint" + defaultHelpPostfix(AuthToken), + Optional: true, + Type: "string", + Sensitive: true, + }, + config.HelpKV{ + Key: config.Comment, + Description: config.DefaultComment, + Optional: true, + Type: "sentence", + }, + } +)