mirror of
https://github.com/minio/minio.git
synced 2024-12-25 14:45:54 -05:00
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.
This commit is contained in:
parent
edf364bf21
commit
83071a3459
@ -33,7 +33,7 @@ import (
|
|||||||
"github.com/minio/minio/internal/config/etcd"
|
"github.com/minio/minio/internal/config/etcd"
|
||||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||||
"github.com/minio/minio/internal/config/identity/openid"
|
"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/config/storageclass"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
iampolicy "github.com/minio/pkg/iam/policy"
|
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)
|
off = !cache.Enabled(kv)
|
||||||
case config.StorageClassSubSys:
|
case config.StorageClassSubSys:
|
||||||
off = !storageclass.Enabled(kv)
|
off = !storageclass.Enabled(kv)
|
||||||
case config.PolicyOPASubSys:
|
case config.PolicyPluginSubSys:
|
||||||
off = !opa.Enabled(kv)
|
off = !polplugin.Enabled(kv)
|
||||||
case config.IdentityOpenIDSubSys:
|
case config.IdentityOpenIDSubSys:
|
||||||
off = !openid.Enabled(kv)
|
off = !openid.Enabled(kv)
|
||||||
case config.IdentityLDAPSubSys:
|
case config.IdentityLDAPSubSys:
|
||||||
|
@ -217,8 +217,8 @@ func getClaimsFromTokenWithSecret(token, secret string) (map[string]interface{},
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If OPA is set, return without any further checks.
|
// If AuthZPlugin is set, return without any further checks.
|
||||||
if globalPolicyOPA != nil {
|
if globalAuthZPlugin != nil {
|
||||||
return claims.Map(), nil
|
return claims.Map(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ import (
|
|||||||
xtls "github.com/minio/minio/internal/config/identity/tls"
|
xtls "github.com/minio/minio/internal/config/identity/tls"
|
||||||
"github.com/minio/minio/internal/config/notify"
|
"github.com/minio/minio/internal/config/notify"
|
||||||
"github.com/minio/minio/internal/config/policy/opa"
|
"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/scanner"
|
||||||
"github.com/minio/minio/internal/config/storageclass"
|
"github.com/minio/minio/internal/config/storageclass"
|
||||||
"github.com/minio/minio/internal/config/subnet"
|
"github.com/minio/minio/internal/config/subnet"
|
||||||
@ -56,6 +57,7 @@ func initHelp() {
|
|||||||
config.IdentityOpenIDSubSys: openid.DefaultKVS,
|
config.IdentityOpenIDSubSys: openid.DefaultKVS,
|
||||||
config.IdentityTLSSubSys: xtls.DefaultKVS,
|
config.IdentityTLSSubSys: xtls.DefaultKVS,
|
||||||
config.PolicyOPASubSys: opa.DefaultKVS,
|
config.PolicyOPASubSys: opa.DefaultKVS,
|
||||||
|
config.PolicyPluginSubSys: polplugin.DefaultKVS,
|
||||||
config.SiteSubSys: config.DefaultSiteKVS,
|
config.SiteSubSys: config.DefaultSiteKVS,
|
||||||
config.RegionSubSys: config.DefaultRegionKVS,
|
config.RegionSubSys: config.DefaultRegionKVS,
|
||||||
config.APISubSys: api.DefaultKVS,
|
config.APISubSys: api.DefaultKVS,
|
||||||
@ -107,8 +109,8 @@ func initHelp() {
|
|||||||
Description: "enable X.509 TLS certificate SSO support",
|
Description: "enable X.509 TLS certificate SSO support",
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.PolicyOPASubSys,
|
Key: config.PolicyPluginSubSys,
|
||||||
Description: "enable external OPA for policy enforcement",
|
Description: "enable Access Management Plugin for policy enforcement",
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.APISubSys,
|
Key: config.APISubSys,
|
||||||
@ -219,6 +221,7 @@ func initHelp() {
|
|||||||
config.IdentityLDAPSubSys: xldap.Help,
|
config.IdentityLDAPSubSys: xldap.Help,
|
||||||
config.IdentityTLSSubSys: xtls.Help,
|
config.IdentityTLSSubSys: xtls.Help,
|
||||||
config.PolicyOPASubSys: opa.Help,
|
config.PolicyOPASubSys: opa.Help,
|
||||||
|
config.PolicyPluginSubSys: polplugin.Help,
|
||||||
config.LoggerWebhookSubSys: logger.Help,
|
config.LoggerWebhookSubSys: logger.Help,
|
||||||
config.AuditWebhookSubSys: logger.HelpWebhook,
|
config.AuditWebhookSubSys: logger.HelpWebhook,
|
||||||
config.AuditKafkaSubSys: logger.HelpKafka,
|
config.AuditKafkaSubSys: logger.HelpKafka,
|
||||||
@ -243,6 +246,10 @@ func initHelp() {
|
|||||||
Key: config.RegionSubSys,
|
Key: config.RegionSubSys,
|
||||||
Description: "[DEPRECATED - use `site` instead] label the location of the server",
|
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)
|
config.RegisterHelpDeprecatedSubSys(deprecatedHelpKVMap)
|
||||||
@ -340,10 +347,21 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case config.PolicyOPASubSys:
|
case config.PolicyOPASubSys:
|
||||||
|
// 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],
|
if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||||
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
|
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
if config.LoggerSubSystems.Contains(subSys) {
|
if config.LoggerSubSystems.Contains(subSys) {
|
||||||
if err := logger.ValidateSubSysConfig(s, subSys); err != nil {
|
if err := logger.ValidateSubSysConfig(s, subSys); err != nil {
|
||||||
@ -523,12 +541,24 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
|||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authZPluginCfg, err := polplugin.LookupConfig(s[config.PolicyPluginSubSys][config.Default],
|
||||||
|
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthZPlugin: %w", err))
|
||||||
|
}
|
||||||
|
if authZPluginCfg.URL == nil {
|
||||||
opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||||
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OPA: %w", err))
|
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
|
||||||
}
|
}
|
||||||
globalPolicyOPA = opa.New(opaCfg)
|
}
|
||||||
|
globalAuthZPlugin = polplugin.New(authZPluginCfg)
|
||||||
|
|
||||||
globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
||||||
globalRootCAs)
|
globalRootCAs)
|
||||||
|
@ -42,7 +42,7 @@ import (
|
|||||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||||
"github.com/minio/minio/internal/config/identity/openid"
|
"github.com/minio/minio/internal/config/identity/openid"
|
||||||
xtls "github.com/minio/minio/internal/config/identity/tls"
|
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/storageclass"
|
||||||
"github.com/minio/minio/internal/config/subnet"
|
"github.com/minio/minio/internal/config/subnet"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
@ -290,8 +290,8 @@ var (
|
|||||||
// Some standard content-types which we strictly dis-allow for compression.
|
// 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"}
|
standardExcludeCompressContentTypes = []string{"video/*", "audio/*", "application/zip", "application/x-gzip", "application/x-zip-compressed", " application/x-compress", "application/x-spoon"}
|
||||||
|
|
||||||
// OPA policy system.
|
// AuthZ Plugin system.
|
||||||
globalPolicyOPA *opa.Opa
|
globalAuthZPlugin *polplugin.AuthZPlugin
|
||||||
|
|
||||||
// Deployment ID - unique per deployment
|
// Deployment ID - unique per deployment
|
||||||
globalDeploymentID string
|
globalDeploymentID string
|
||||||
|
@ -681,7 +681,7 @@ func (sys *IAMSys) SetTempUser(ctx context.Context, accessKey string, cred auth.
|
|||||||
return errServerNotInitialized
|
return errServerNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalPolicyOPA != nil {
|
if globalAuthZPlugin != nil {
|
||||||
// If OPA is set, we do not need to set a policy mapping.
|
// If OPA is set, we do not need to set a policy mapping.
|
||||||
policyName = ""
|
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.
|
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||||
func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
|
func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
|
||||||
// If opa is configured, use OPA always.
|
// If opa is configured, use OPA always.
|
||||||
if globalPolicyOPA != nil {
|
if globalAuthZPlugin != nil {
|
||||||
ok, err := globalPolicyOPA.IsAllowed(args)
|
ok, err := globalAuthZPlugin.IsAllowed(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(GlobalContext, err)
|
logger.LogIf(GlobalContext, err)
|
||||||
}
|
}
|
||||||
|
@ -383,7 +383,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithSSO(w http.ResponseWriter, r *http.Requ
|
|||||||
policyName = globalIAMSys.CurrentPolicies(policies)
|
policyName = globalIAMSys.CurrentPolicies(policies)
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalPolicyOPA == nil {
|
if globalAuthZPlugin == nil {
|
||||||
if !ok {
|
if !ok {
|
||||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
|
||||||
fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID()))
|
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.
|
// Check if this user or their groups have a policy applied.
|
||||||
ldapPolicies, _ := globalIAMSys.PolicyDBGet(ldapUserDN, false, groupDistNames...)
|
ldapPolicies, _ := globalIAMSys.PolicyDBGet(ldapUserDN, false, groupDistNames...)
|
||||||
if len(ldapPolicies) == 0 && globalPolicyOPA == nil {
|
if len(ldapPolicies) == 0 && globalAuthZPlugin == nil {
|
||||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
|
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",
|
fmt.Errorf("expecting a policy to be set for user `%s` or one of their groups: `%s` - rejecting this request",
|
||||||
ldapUserDN, strings.Join(groupDistNames, "`,`")))
|
ldapUserDN, strings.Join(groupDistNames, "`,`")))
|
||||||
|
159
docs/iam/access-management-plugin.md
Normal file
159
docs/iam/access-management-plugin.md
Normal file
@ -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:
|
||||||
|
|
||||||
|
<details><summary>Request Body Sample</summary>
|
||||||
|
|
||||||
|
```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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
83
docs/iam/access-manager-plugin.go
Normal file
83
docs/iam/access-manager-plugin.go
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
# OPA Quickstart Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io)
|
# 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 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
|
## Get started
|
||||||
|
|
||||||
### 1. Start OPA in a container
|
### 1. Start OPA in a container
|
||||||
@ -48,10 +50,10 @@ curl -X PUT --data-binary @example.rego \
|
|||||||
|
|
||||||
### 4. Setup MinIO with OPA
|
### 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
|
```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_CI_CD=1
|
||||||
export MINIO_ROOT_USER=minio
|
export MINIO_ROOT_USER=minio
|
||||||
export MINIO_ROOT_PASSWORD=minio123
|
export MINIO_ROOT_PASSWORD=minio123
|
||||||
@ -77,6 +79,3 @@ mc cat foo/test/issue
|
|||||||
mc cp /etc/issue myminio/test/issue2
|
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)
|
|
@ -69,6 +69,7 @@ const (
|
|||||||
const (
|
const (
|
||||||
CredentialsSubSys = "credentials"
|
CredentialsSubSys = "credentials"
|
||||||
PolicyOPASubSys = "policy_opa"
|
PolicyOPASubSys = "policy_opa"
|
||||||
|
PolicyPluginSubSys = "policy_plugin"
|
||||||
IdentityOpenIDSubSys = "identity_openid"
|
IdentityOpenIDSubSys = "identity_openid"
|
||||||
IdentityLDAPSubSys = "identity_ldap"
|
IdentityLDAPSubSys = "identity_ldap"
|
||||||
IdentityTLSSubSys = "identity_tls"
|
IdentityTLSSubSys = "identity_tls"
|
||||||
@ -141,6 +142,7 @@ var SubSystems = set.CreateStringSet(
|
|||||||
AuditWebhookSubSys,
|
AuditWebhookSubSys,
|
||||||
AuditKafkaSubSys,
|
AuditKafkaSubSys,
|
||||||
PolicyOPASubSys,
|
PolicyOPASubSys,
|
||||||
|
PolicyPluginSubSys,
|
||||||
IdentityLDAPSubSys,
|
IdentityLDAPSubSys,
|
||||||
IdentityOpenIDSubSys,
|
IdentityOpenIDSubSys,
|
||||||
IdentityTLSSubSys,
|
IdentityTLSSubSys,
|
||||||
@ -183,6 +185,7 @@ var SubSystemsSingleTargets = set.CreateStringSet([]string{
|
|||||||
StorageClassSubSys,
|
StorageClassSubSys,
|
||||||
CompressionSubSys,
|
CompressionSubSys,
|
||||||
PolicyOPASubSys,
|
PolicyOPASubSys,
|
||||||
|
PolicyPluginSubSys,
|
||||||
IdentityLDAPSubSys,
|
IdentityLDAPSubSys,
|
||||||
IdentityTLSSubSys,
|
IdentityTLSSubSys,
|
||||||
HealSubSys,
|
HealSubSys,
|
||||||
|
@ -28,13 +28,13 @@ var (
|
|||||||
Help = config.HelpKVS{
|
Help = config.HelpKVS{
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: URL,
|
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",
|
Type: "url",
|
||||||
Sensitive: true,
|
Sensitive: true,
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: AuthToken,
|
Key: AuthToken,
|
||||||
Description: "authorization token for OPA endpoint" + defaultHelpPostfix(AuthToken),
|
Description: "[DEPRECATED] authorization token for OPA endpoint" + defaultHelpPostfix(AuthToken),
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Type: "string",
|
Type: "string",
|
||||||
Sensitive: true,
|
Sensitive: true,
|
||||||
|
223
internal/config/policy/plugin/config.go
Normal file
223
internal/config/policy/plugin/config.go
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
49
internal/config/policy/plugin/help.go
Normal file
49
internal/config/policy/plugin/help.go
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user