mirror of https://github.com/minio/minio.git
Browse Source
Add support for Identity Management Plugin (#14913)
Add support for Identity Management Plugin (#14913)
- Adds an STS API `AssumeRoleWithCustomToken` that can be used to authenticate via the Id. Mgmt. Plugin. - Adds a sample identity manager plugin implementation - Add doc for plugin and STS API - Add an example program using go SDK for AssumeRoleWithCustomTokenpull/14983/head

committed by
GitHub

No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 888 additions and 28 deletions
-
3cmd/admin-handlers-config-kv.go
-
31cmd/config-current.go
-
4cmd/globals.go
-
52cmd/iam.go
-
13cmd/sts-datatypes.go
-
6cmd/sts-errors.go
-
139cmd/sts-handlers.go
-
7cmd/stserrorcode_string.go
-
76docs/iam/identity-management-plugin.md
-
86docs/iam/identity-manager-plugin.go
-
121docs/sts/custom-token-identity.go
-
53docs/sts/custom-token-identity.md
-
3internal/config/config.go
-
322internal/config/identity/plugin/config.go
@ -0,0 +1,76 @@ |
|||
# Identity Management Plugin Guide [](https://slack.minio.io) |
|||
|
|||
## Introduction |
|||
|
|||
To enable the integration of custom authentication methods, MinIO can be configured with an Identity Management Plugin webhook. When configured, this plugin enables the `AssumeRoleWithCustomToken` STS API extension. A user or application can now present a token to the `AssumeRoleWithCustomToken` API, and MinIO verifies this token by sending it to the Identity Management Plugin webhook. This plugin responds with some information and MinIO is able to generate temporary STS credentials to interact with object storage. |
|||
|
|||
The authentication flow is similar to that of OpenID, however the token is "opaque" to MinIO - it is simply sent to the plugin for verification. CAVEAT: There is no console UI integration for this method of authentication and it is intended primarily for machine authentication. |
|||
|
|||
It can be configured via MinIO's standard configuration API (i.e. using `mc admin config set/get`), or equivalently with environment variables. For brevity we show only environment variables here: |
|||
|
|||
```sh |
|||
$ mc admin config set myminio identity_plugin --env |
|||
KEY: |
|||
identity_plugin enable Identity Plugin via external hook |
|||
|
|||
ARGS: |
|||
MINIO_IDENTITY_PLUGIN_URL* (url) plugin hook endpoint (HTTP(S)) e.g. "http://localhost:8181/path/to/endpoint" |
|||
MINIO_IDENTITY_PLUGIN_AUTH_TOKEN (string) authorization token for plugin hook endpoint |
|||
MINIO_IDENTITY_PLUGIN_ROLE_POLICY* (string) policies to apply for plugin authorized users |
|||
MINIO_IDENTITY_PLUGIN_ROLE_ID (string) unique ID to generate the ARN |
|||
MINIO_IDENTITY_PLUGIN_COMMENT (sentence) optionally add a comment to this setting |
|||
``` |
|||
|
|||
If provided, the auth token parameter is sent as an authorization header. |
|||
|
|||
`MINIO_IDENTITY_PLUGIN_ROLE_POLICY` is a required parameter and can be list of comma separated policy names. |
|||
|
|||
On setting up the plugin, the MinIO server prints the Role ARN to its log. The Role ARN is generated by default based on the given plugin URL. To avoid this and use a configurable value set a unique role ID via `MINIO_IDENTITY_PLUGIN_ROLE_ID`. |
|||
|
|||
## REST API call to plugin |
|||
|
|||
To verify the custom token presented in the `AssumeRoleWithCustomToken` API, MinIO makes a POST request to the configured identity management plugin endpoint and expects a response with some details as shown below: |
|||
|
|||
### Request `POST` to plugin endpoint |
|||
|
|||
Query parameters: |
|||
|
|||
| Parameter Name | Value Type | Purpose | |
|||
|----------------|------------|-------------------------------------------------------------------------| |
|||
| token | string | Token from the AssumeRoleWithCustomToken call for external verification | |
|||
|
|||
### Response |
|||
|
|||
If the token is valid and access is approved, the plugin must return a `200` (OK) HTTP status code. |
|||
|
|||
A `200 OK` Response should have `application/json` content-type and body with the following structure: |
|||
|
|||
```json |
|||
{ |
|||
"user": <string>, |
|||
"maxValiditySeconds": <integer>, |
|||
"claims": <key-value-pairs> |
|||
} |
|||
``` |
|||
|
|||
| Parameter Name | Value Type | Purpose | |
|||
|--------------------|-----------------------------------------|--------------------------------------------------------| |
|||
| user | string | Identifier for owner of requested credentials | |
|||
| maxValiditySeconds | integer (>= 900 seconds and < 365 days) | Maximum allowed expiry duration for the credentials | |
|||
| claims | key-value pairs | Claims to be associated with the requested credentials | |
|||
|
|||
The keys "exp", "parent" and "sub" in the `claims` object are reserved and if present are ignored by MinIO. |
|||
|
|||
If the token is not valid or access is not approved, the plugin must return a `403` (forbidden) HTTP status code. The body must have an `application/json` content-type with the following structure: |
|||
|
|||
```json |
|||
{ |
|||
"reason": <string> |
|||
} |
|||
``` |
|||
|
|||
The reason message is returned to the client. |
|||
|
|||
## Example Plugin Implementation |
|||
|
|||
A toy example for the Identity Management Plugin is given [here](./identity-manager-plugin.go). |
@ -0,0 +1,86 @@ |
|||
//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" |
|||
"errors" |
|||
"fmt" |
|||
"log" |
|||
"net/http" |
|||
) |
|||
|
|||
func writeErrorResponse(w http.ResponseWriter, err error) { |
|||
w.WriteHeader(http.StatusBadRequest) |
|||
json.NewEncoder(w).Encode(map[string]string{ |
|||
"reason": fmt.Sprintf("%v", err), |
|||
}) |
|||
} |
|||
|
|||
type Resp struct { |
|||
User string `json:"user"` |
|||
MaxValiditySeconds int `json:"maxValiditySeconds"` |
|||
Claims map[string]interface{} `json:"claims"` |
|||
} |
|||
|
|||
var tokens map[string]Resp = map[string]Resp{ |
|||
"aaa": { |
|||
User: "Alice", |
|||
MaxValiditySeconds: 3600, |
|||
Claims: map[string]interface{}{ |
|||
"groups": []string{"data-science"}, |
|||
}, |
|||
}, |
|||
"bbb": { |
|||
User: "Bart", |
|||
MaxValiditySeconds: 3600, |
|||
Claims: map[string]interface{}{ |
|||
"groups": []string{"databases"}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
func mainHandler(w http.ResponseWriter, r *http.Request) { |
|||
token := r.FormValue("token") |
|||
if token == "" { |
|||
writeErrorResponse(w, errors.New("token parameter not given")) |
|||
return |
|||
} |
|||
|
|||
rsp, ok := tokens[token] |
|||
if !ok { |
|||
w.WriteHeader(http.StatusForbidden) |
|||
return |
|||
} |
|||
|
|||
fmt.Printf("Allowed for token: %s user: %s\n", token, rsp.User) |
|||
|
|||
w.WriteHeader(http.StatusOK) |
|||
json.NewEncoder(w).Encode(rsp) |
|||
return |
|||
} |
|||
|
|||
func main() { |
|||
http.HandleFunc("/", mainHandler) |
|||
|
|||
log.Print("Listing on :8081") |
|||
log.Fatal(http.ListenAndServe(":8081", nil)) |
|||
} |
@ -0,0 +1,121 @@ |
|||
//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 ( |
|||
"context" |
|||
"flag" |
|||
"fmt" |
|||
"log" |
|||
"net/url" |
|||
"time" |
|||
|
|||
"github.com/minio/minio-go/v7" |
|||
cr "github.com/minio/minio-go/v7/pkg/credentials" |
|||
) |
|||
|
|||
var ( |
|||
// LDAP integrated Minio endpoint
|
|||
stsEndpoint string |
|||
|
|||
// token to use with AssumeRoleWithCustomToken
|
|||
token string |
|||
|
|||
// Role ARN to use
|
|||
roleArn string |
|||
|
|||
// Display credentials flag
|
|||
displayCreds bool |
|||
|
|||
// Credential expiry duration
|
|||
expiryDuration time.Duration |
|||
|
|||
// Bucket to list
|
|||
bucketToList string |
|||
) |
|||
|
|||
func init() { |
|||
flag.StringVar(&stsEndpoint, "sts-ep", "http://localhost:9000", "STS endpoint") |
|||
flag.StringVar(&token, "t", "", "Token to use with AssumeRoleWithCustomToken STS API (required)") |
|||
flag.StringVar(&roleArn, "r", "", "RoleARN to use with the request (required)") |
|||
flag.BoolVar(&displayCreds, "d", false, "Only show generated credentials") |
|||
flag.DurationVar(&expiryDuration, "e", 0, "Request a duration of validity for the generated credential") |
|||
flag.StringVar(&bucketToList, "b", "mybucket", "Bucket to list (defaults to mybucket)") |
|||
} |
|||
|
|||
func main() { |
|||
flag.Parse() |
|||
if token == "" || roleArn == "" { |
|||
flag.PrintDefaults() |
|||
return |
|||
} |
|||
|
|||
// The credentials package in minio-go provides an interface to call the
|
|||
// AssumeRoleWithCustomToken STS API.
|
|||
|
|||
var opts []cr.CustomTokenOpt |
|||
if expiryDuration != 0 { |
|||
opts = append(opts, cr.CustomTokenValidityOpt(expiryDuration)) |
|||
} |
|||
|
|||
// Initialize
|
|||
li, err := cr.NewCustomTokenCredentials(stsEndpoint, token, roleArn, opts...) |
|||
if err != nil { |
|||
log.Fatalf("Error initializing CustomToken Identity: %v", err) |
|||
} |
|||
|
|||
v, err := li.Get() |
|||
if err != nil { |
|||
log.Fatalf("Error retrieving STS credentials: %v", err) |
|||
} |
|||
|
|||
if displayCreds { |
|||
fmt.Println("Only displaying credentials:") |
|||
fmt.Println("AccessKeyID:", v.AccessKeyID) |
|||
fmt.Println("SecretAccessKey:", v.SecretAccessKey) |
|||
fmt.Println("SessionToken:", v.SessionToken) |
|||
return |
|||
} |
|||
|
|||
// Use generated credentials to authenticate with MinIO server
|
|||
stsEndpointURL, err := url.Parse(stsEndpoint) |
|||
if err != nil { |
|||
log.Fatalf("Error parsing sts endpoint: %v", err) |
|||
} |
|||
copts := &minio.Options{ |
|||
Creds: li, |
|||
Secure: stsEndpointURL.Scheme == "https", |
|||
} |
|||
minioClient, err := minio.New(stsEndpointURL.Host, copts) |
|||
if err != nil { |
|||
log.Fatalf("Error initializing client: ", err) |
|||
} |
|||
|
|||
// Use minIO Client object normally like the regular client.
|
|||
fmt.Printf("Calling list objects on bucket named `%s` with temp creds:\n===\n", bucketToList) |
|||
objCh := minioClient.ListObjects(context.Background(), bucketToList, minio.ListObjectsOptions{}) |
|||
for obj := range objCh { |
|||
if obj.Err != nil { |
|||
log.Fatalf("Listing error: %v", obj.Err) |
|||
} |
|||
fmt.Printf("Key: %s\nSize: %d\nLast Modified: %s\n===\n", obj.Key, obj.Size, obj.LastModified) |
|||
} |
|||
} |
@ -0,0 +1,53 @@ |
|||
# AssumeRoleWithCustomToken [](https://slack.min.io) |
|||
|
|||
## Introduction |
|||
|
|||
To integrate with custom authentication methods using the [Identity Management Plugin](../iam/identity-management-plugin.md)), MinIO provides an STS API extension called `AssumeRoleWithCustomToken`. |
|||
|
|||
After configuring the plugin, use the generated Role ARN with `AssumeRoleWithCustomToken` to get temporary credentials to access object storage. |
|||
|
|||
## API Request |
|||
|
|||
To make an STS API request with this method, send a POST request to the MinIO endpoint with following query parameters: |
|||
|
|||
| Parameter | Type | Required | | |
|||
|-----------------|---------|----------|----------------------------------------------------------------------| |
|||
| Action | String | Yes | Value must be `AssumeRoleWithCustomToken` | |
|||
| Version | String | Yes | Value must be `2011-06-15` | |
|||
| Token | String | Yes | Token to be authenticated by identity plugin | |
|||
| RoleArn | String | Yes | Must match the Role ARN generated for the identity plugin | |
|||
| DurationSeconds | Integer | No | Duration of validity of generated credentials. Must be at least 900. | |
|||
|
|||
The validity duration of the generated STS credentials is the minimum of the `DurationSeconds` parameter (if passed) and the validity duration returned by the Identity Management Plugin. |
|||
|
|||
## API Response |
|||
|
|||
XML response for this API is similar to [AWS STS AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html#API_AssumeRoleWithWebIdentity_ResponseElements) |
|||
|
|||
## Example request and response |
|||
|
|||
Sample request with `curl`: |
|||
|
|||
```sh |
|||
curl -XPOST 'http://localhost:9001/?Action=AssumeRoleWithCustomToken&Version=2011-06-15&Token=aaa&RoleArn=arn:minio:iam:::role/idmp-vGxBdLkOc8mQPU1-UQbBh-yWWVQ' |
|||
``` |
|||
|
|||
Prettified Response: |
|||
|
|||
```xml |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<AssumeRoleWithCustomTokenResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/"> |
|||
<AssumeRoleWithCustomTokenResult> |
|||
<Credentials> |
|||
<AccessKeyId>24Y5H9VHE14H47GEOKCX</AccessKeyId> |
|||
<SecretAccessKey>H+aBfQ9B1AeWWb++84hvp4tlFBo9aP+hUTdLFIeg</SecretAccessKey> |
|||
<Expiration>2022-05-25T19:56:34Z</Expiration> |
|||
<SessionToken>eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiIyNFk1SDlWSEUxNEg0N0dFT0tDWCIsImV4cCI6MTY1MzUwODU5NCwiZ3JvdXBzIjpbImRhdGEtc2NpZW5jZSJdLCJwYXJlbnQiOiJjdXN0b206QWxpY2UiLCJyb2xlQXJuIjoiYXJuOm1pbmlvOmlhbTo6OnJvbGUvaWRtcC14eHgiLCJzdWIiOiJjdXN0b206QWxpY2UifQ.1tO1LmlUNXiy-wl-ZbkJLWTpaPlhaGqHehsi21lNAmAGCImHHsPb-GA4lRq6GkvHAODN5ZYCf_S-OwpOOdxFwA</SessionToken> |
|||
</Credentials> |
|||
<AssumedUser>custom:Alice</AssumedUser> |
|||
</AssumeRoleWithCustomTokenResult> |
|||
<ResponseMetadata> |
|||
<RequestId>16F26E081E36DE63</RequestId> |
|||
</ResponseMetadata> |
|||
</AssumeRoleWithCustomTokenResponse> |
|||
``` |
@ -0,0 +1,322 @@ |
|||
// 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" |
|||
"crypto/sha1" |
|||
"encoding/base64" |
|||
"encoding/json" |
|||
"fmt" |
|||
"io" |
|||
"net/http" |
|||
"net/url" |
|||
"regexp" |
|||
|
|||
"github.com/minio/minio/internal/arn" |
|||
"github.com/minio/minio/internal/config" |
|||
"github.com/minio/pkg/env" |
|||
xnet "github.com/minio/pkg/net" |
|||
) |
|||
|
|||
// Authentication Plugin config and env variables
|
|||
const ( |
|||
URL = "url" |
|||
AuthToken = "auth_token" |
|||
RolePolicy = "role_policy" |
|||
RoleID = "role_id" |
|||
|
|||
EnvIdentityPluginURL = "MINIO_IDENTITY_PLUGIN_URL" |
|||
EnvIdentityPluginAuthToken = "MINIO_IDENTITY_PLUGIN_AUTH_TOKEN" |
|||
EnvIdentityPluginRolePolicy = "MINIO_IDENTITY_PLUGIN_ROLE_POLICY" |
|||
EnvIdentityPluginRoleID = "MINIO_IDENTITY_PLUGIN_ROLE_ID" |
|||
) |
|||
|
|||
var ( |
|||
// DefaultKVS - default config for AuthN plugin config
|
|||
DefaultKVS = config.KVS{ |
|||
config.KV{ |
|||
Key: URL, |
|||
Value: "", |
|||
}, |
|||
config.KV{ |
|||
Key: AuthToken, |
|||
Value: "", |
|||
}, |
|||
config.KV{ |
|||
Key: RolePolicy, |
|||
Value: "", |
|||
}, |
|||
config.KV{ |
|||
Key: RoleID, |
|||
Value: "", |
|||
}, |
|||
} |
|||
|
|||
defaultHelpPostfix = func(key string) string { |
|||
return config.DefaultHelpPostfix(DefaultKVS, key) |
|||
} |
|||
|
|||
// Help for Identity Plugin
|
|||
Help = config.HelpKVS{ |
|||
config.HelpKV{ |
|||
Key: URL, |
|||
Description: `plugin hook endpoint (HTTP(S)) e.g. "http://localhost:8181/path/to/endpoint"` + defaultHelpPostfix(URL), |
|||
Type: "url", |
|||
}, |
|||
config.HelpKV{ |
|||
Key: AuthToken, |
|||
Description: "authorization token for plugin hook endpoint" + defaultHelpPostfix(AuthToken), |
|||
Optional: true, |
|||
Type: "string", |
|||
Sensitive: true, |
|||
}, |
|||
config.HelpKV{ |
|||
Key: RolePolicy, |
|||
Description: "policies to apply for plugin authorized users" + defaultHelpPostfix(RolePolicy), |
|||
Type: "string", |
|||
}, |
|||
config.HelpKV{ |
|||
Key: RoleID, |
|||
Description: "unique ID to generate the ARN" + defaultHelpPostfix(RoleID), |
|||
Optional: true, |
|||
Type: "string", |
|||
}, |
|||
config.HelpKV{ |
|||
Key: config.Comment, |
|||
Description: config.DefaultComment, |
|||
Optional: true, |
|||
Type: "sentence", |
|||
}, |
|||
} |
|||
) |
|||
|
|||
// Allows only Base64 URL encoding characters.
|
|||
var validRoleIDRegex = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) |
|||
|
|||
// Args for authentication plugin.
|
|||
type Args struct { |
|||
URL *xnet.URL |
|||
AuthToken string |
|||
Transport http.RoundTripper |
|||
CloseRespFn func(r io.ReadCloser) |
|||
|
|||
RolePolicy string |
|||
RoleARN arn.ARN |
|||
} |
|||
|
|||
// Validate - validate 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 |
|||
} |
|||
|
|||
// AuthNPlugin - implements pluggable authentication via webhook.
|
|||
type AuthNPlugin struct { |
|||
args Args |
|||
client *http.Client |
|||
} |
|||
|
|||
// Enabled returns if AuthNPlugin is enabled.
|
|||
func Enabled(kvs config.KVS) bool { |
|||
return kvs.Get(URL) != "" |
|||
} |
|||
|
|||
// LookupConfig lookup AuthNPlugin from config, override with any ENVs.
|
|||
func LookupConfig(kv config.KVS, transport *http.Transport, closeRespFn func(io.ReadCloser), serverRegion string) (Args, error) { |
|||
args := Args{} |
|||
|
|||
if err := config.CheckValidKeys(config.IdentityPluginSubSys, kv, DefaultKVS); err != nil { |
|||
return args, err |
|||
} |
|||
|
|||
pluginURL := env.Get(EnvIdentityPluginURL, kv.Get(URL)) |
|||
if pluginURL == "" { |
|||
return args, nil |
|||
} |
|||
|
|||
authToken := env.Get(EnvIdentityPluginAuthToken, kv.Get(AuthToken)) |
|||
|
|||
u, err := xnet.ParseHTTPURL(pluginURL) |
|||
if err != nil { |
|||
return args, err |
|||
} |
|||
|
|||
rolePolicy := env.Get(EnvIdentityPluginRolePolicy, kv.Get(RolePolicy)) |
|||
if rolePolicy == "" { |
|||
return args, config.Errorf("A role policy must be specified for Identity Management Plugin") |
|||
} |
|||
|
|||
resourceID := "idmp-" |
|||
roleID := env.Get(EnvIdentityPluginRoleID, kv.Get(RoleID)) |
|||
if roleID == "" { |
|||
// We use a hash of the plugin URL so that the ARN remains
|
|||
// constant across restarts.
|
|||
h := sha1.New() |
|||
h.Write([]byte(pluginURL)) |
|||
bs := h.Sum(nil) |
|||
resourceID += base64.RawURLEncoding.EncodeToString(bs) |
|||
} else { |
|||
// Check that the roleID is restricted to URL safe characters
|
|||
// (base64 URL encoding chars).
|
|||
if !validRoleIDRegex.MatchString(roleID) { |
|||
return args, config.Errorf("Role ID must match the regexp `^[a-zA-Z0-9_-]+$`") |
|||
} |
|||
|
|||
// Use the user provided ID here.
|
|||
resourceID += roleID |
|||
} |
|||
|
|||
roleArn, err := arn.NewIAMRoleARN(resourceID, serverRegion) |
|||
if err != nil { |
|||
return args, config.Errorf("unable to generate ARN from the plugin config: %v", err) |
|||
} |
|||
|
|||
args = Args{ |
|||
URL: u, |
|||
AuthToken: authToken, |
|||
Transport: transport, |
|||
CloseRespFn: closeRespFn, |
|||
RolePolicy: rolePolicy, |
|||
RoleARN: roleArn, |
|||
} |
|||
if err = args.Validate(); err != nil { |
|||
return args, err |
|||
} |
|||
return args, nil |
|||
} |
|||
|
|||
// New - initializes Authorization Management Plugin.
|
|||
func New(args Args) *AuthNPlugin { |
|||
if args.URL == nil || args.URL.Scheme == "" && args.AuthToken == "" { |
|||
return nil |
|||
} |
|||
return &AuthNPlugin{ |
|||
args: args, |
|||
client: &http.Client{Transport: args.Transport}, |
|||
} |
|||
} |
|||
|
|||
// AuthNSuccessResponse - represents the response from the authentication plugin
|
|||
// service.
|
|||
type AuthNSuccessResponse struct { |
|||
User string `json:"user"` |
|||
MaxValiditySeconds int `json:"maxValiditySeconds"` |
|||
Claims map[string]interface{} `json:"claims"` |
|||
} |
|||
|
|||
// AuthNErrorResponse - represents an error response from the authN plugin.
|
|||
type AuthNErrorResponse struct { |
|||
Reason string `json:"reason"` |
|||
} |
|||
|
|||
// AuthNResponse - represents a result of the authentication operation.
|
|||
type AuthNResponse struct { |
|||
Success *AuthNSuccessResponse |
|||
Failure *AuthNErrorResponse |
|||
} |
|||
|
|||
const ( |
|||
minValidityDurationSeconds int = 900 |
|||
maxValidityDurationSeconds int = 365 * 24 * 3600 |
|||
) |
|||
|
|||
// Authenticate authenticates the token with the external hook endpoint and
|
|||
// returns a parent user, max expiry duration for the authentication and a set
|
|||
// of claims.
|
|||
func (o *AuthNPlugin) Authenticate(roleArn arn.ARN, token string) (AuthNResponse, error) { |
|||
if o == nil { |
|||
return AuthNResponse{}, nil |
|||
} |
|||
|
|||
if roleArn != o.args.RoleARN { |
|||
return AuthNResponse{}, fmt.Errorf("Invalid role ARN value: %s", roleArn.String()) |
|||
} |
|||
|
|||
var u url.URL = url.URL(*o.args.URL) |
|||
q := u.Query() |
|||
q.Set("token", token) |
|||
u.RawQuery = q.Encode() |
|||
|
|||
req, err := http.NewRequest(http.MethodPost, u.String(), nil) |
|||
if err != nil { |
|||
return AuthNResponse{}, err |
|||
} |
|||
|
|||
if o.args.AuthToken != "" { |
|||
req.Header.Set("Authorization", o.args.AuthToken) |
|||
} |
|||
|
|||
resp, err := o.client.Do(req) |
|||
if err != nil { |
|||
return AuthNResponse{}, err |
|||
} |
|||
defer o.args.CloseRespFn(resp.Body) |
|||
|
|||
switch resp.StatusCode { |
|||
case 200: |
|||
var result AuthNSuccessResponse |
|||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { |
|||
return AuthNResponse{}, err |
|||
} |
|||
|
|||
if result.MaxValiditySeconds < minValidityDurationSeconds || result.MaxValiditySeconds > maxValidityDurationSeconds { |
|||
return AuthNResponse{}, fmt.Errorf("Plugin returned an invalid validity duration (%d) - should be between %d and %d", |
|||
result.MaxValiditySeconds, minValidityDurationSeconds, maxValidityDurationSeconds) |
|||
} |
|||
|
|||
return AuthNResponse{ |
|||
Success: &result, |
|||
}, nil |
|||
|
|||
case 403: |
|||
var result AuthNErrorResponse |
|||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { |
|||
return AuthNResponse{}, err |
|||
} |
|||
return AuthNResponse{ |
|||
Failure: &result, |
|||
}, nil |
|||
|
|||
default: |
|||
return AuthNResponse{}, fmt.Errorf("Invalid status code %d from auth plugin", resp.StatusCode) |
|||
} |
|||
} |
|||
|
|||
// GetRoleInfo - returns ARN to policies map.
|
|||
func (o *AuthNPlugin) GetRoleInfo() map[arn.ARN]string { |
|||
return map[arn.ARN]string{ |
|||
o.args.RoleARN: o.args.RolePolicy, |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue