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