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 AssumeRoleWithCustomToken
This commit is contained in:
Aditya Manthramurthy
2022-05-26 17:58:09 -07:00
committed by GitHub
parent 5c81d0d89a
commit 464b9d7c80
14 changed files with 888 additions and 28 deletions

View File

@@ -0,0 +1,76 @@
# Identity Management Plugin Guide [![Slack](https://slack.minio.io/slack?type=svg)](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).

View File

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

View File

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

View File

@@ -0,0 +1,53 @@
# AssumeRoleWithCustomToken [![Slack](https://slack.min.io/slack?type=svg)](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>
```