github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/remote/access/access.go (about) 1 package access 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 8 golog "github.com/ipfs/go-log" 9 "github.com/qri-io/qri/dsref" 10 "github.com/qri-io/qri/profile" 11 ) 12 13 // special tokens in access grammer 14 const ( 15 matchAll = "*" 16 matchSubject = "_subject" 17 ) 18 19 var ( 20 // ErrAccessDenied is returned by policy enforce 21 ErrAccessDenied = fmt.Errorf("access denied") 22 log = golog.Logger("access") 23 // DefaultAccessControlPolicyFilename is the file name for the policy 24 // expected file is format yaml 25 DefaultAccessControlPolicyFilename = "access_control_policy.yaml" 26 ) 27 28 // Effect is the set of outcomes a rule can have 29 type Effect string 30 31 const ( 32 // EffectAllow describes a rule that adds permissions 33 EffectAllow = Effect("allow") 34 // EffectDeny describes a rule that removes permissions 35 EffectDeny = Effect("deny") 36 ) 37 38 // Policy is a set of rules 39 type Policy []Rule 40 41 // Rule is a permissions statement. It determines who (subject) can/can't 42 // (effect) do something (actions) to things (resources) 43 type Rule struct { 44 Title string // human-legible title for the rule, informative only 45 Subject string // User this rule is about 46 Resources Resources // Thing being accessed. eg: a dataset, 47 Actions Actions // Thing user can do 48 Effect Effect // "allow" or "deny" 49 } 50 51 type rule Rule 52 53 // UnmarshalJSON unmarshals the slice of bytes into a Rule 54 func (r *Rule) UnmarshalJSON(d []byte) error { 55 _rule := rule{} 56 if err := json.Unmarshal(d, &_rule); err != nil { 57 return err 58 } 59 60 rule := Rule(_rule) 61 if err := rule.Validate(); err != nil { 62 return err 63 } 64 65 *r = rule 66 return nil 67 } 68 69 // Validate returns a descriptive error if the rule is not well-formed 70 func (r *Rule) Validate() error { 71 if r.Subject == "" { 72 return fmt.Errorf("rule.Subject is required") 73 } 74 if r.Effect != EffectAllow && r.Effect != EffectDeny { 75 return fmt.Errorf(`rule.Effect must be one of ("allow"|"deny")`) 76 } 77 if len(r.Resources) == 0 { 78 return fmt.Errorf("rule.Resources field is required") 79 } 80 if len(r.Actions) == 0 { 81 return fmt.Errorf("rule.Actions field is required") 82 } 83 return nil 84 } 85 86 // Enforce evaluates a request against the policy, returning either nil or 87 // ErrAccessDenied 88 func (pol Policy) Enforce(subject *profile.Profile, resource, action string) error { 89 log.Debugf("policy.Enforce username=%q resource=%q action=%q", subject.Peername, resource, action) 90 rsc, err := ParseResource(resource) 91 if err != nil { 92 return err 93 } 94 95 act, err := ParseAction(action) 96 if err != nil { 97 return err 98 } 99 100 for _, rule := range pol { 101 log.Debugf("rule=%q effect=%q subject=%t resources=%t actions=%t", rule.Title, rule.Effect, 102 (rule.Subject == subject.ID.Encode() || rule.Subject == matchAll), 103 rule.Resources.Contains(rsc, subject.Peername), 104 rule.Actions.Contains(act), 105 ) 106 107 if rule.Effect == EffectAllow && 108 (rule.Subject == subject.ID.Encode() || rule.Subject == matchAll) && 109 rule.Resources.Contains(rsc, subject.Peername) && 110 rule.Actions.Contains(act) { 111 log.Debugf("matched rule title=%q", rule.Title) 112 return nil 113 } 114 } 115 return ErrAccessDenied 116 } 117 118 // Resources is a collection of resoureces 119 type Resources []Resource 120 121 // Contains iterates all Resources in the slice, returns true for the first 122 // resource that contains the given resource 123 func (rs Resources) Contains(b Resource, subjectUsername string) bool { 124 for _, r := range rs { 125 if r.Contains(b, subjectUsername) { 126 return true 127 } 128 } 129 return false 130 } 131 132 // Resource is a stateful thing in qri 133 type Resource []string 134 135 // MustParseResource wraps ParseResource, panics on error. Useful for tests 136 func MustParseResource(str string) Resource { 137 rsc, err := ParseResource(str) 138 if err != nil { 139 panic(err) 140 } 141 return rsc 142 } 143 144 // ParseResource constructs a resource from a string 145 func ParseResource(str string) (Resource, error) { 146 if str == "" { 147 return nil, fmt.Errorf("resource string cannot be empty") 148 } 149 150 rsc := strings.Split(str, ":") 151 152 foundStar := false 153 for _, name := range rsc { 154 if name == "*" { 155 if foundStar { 156 return nil, fmt.Errorf(`invalid resource string %q. '*' character cannot occur twice`, str) 157 } 158 foundStar = true 159 } else if foundStar { 160 return nil, fmt.Errorf(`invalid resource string %q. '*' must come last`, str) 161 } 162 } 163 164 return rsc, nil 165 } 166 167 // MarshalJSON marshals the resource into a string separated by ":" 168 func (r Resource) MarshalJSON() ([]byte, error) { 169 return []byte(strings.Join(r, ":")), nil 170 } 171 172 // UnmarshalJSON unmarshals a slice of bytes into a Resource 173 func (r *Resource) UnmarshalJSON(data []byte) error { 174 var str string 175 if err := json.Unmarshal(data, &str); err != nil { 176 return err 177 } 178 179 rsc, err := ParseResource(str) 180 if err != nil { 181 return err 182 } 183 184 *r = rsc 185 return nil 186 } 187 188 // Contains determins if the subject is referenced in the resource 189 // returns true if the rule's resource contains the `matchAll` symbol 190 // and returns true if the rule's resource contains the `matchSubject` 191 // and the subjectUsername is in the given resource (allows us to create rules 192 // that say, "only allow subjects to do this action, if the resource matches 193 // the subject's name" 194 func (r Resource) Contains(b Resource, subjectUsername string) bool { 195 if len(r) > len(b) { 196 return false 197 } 198 199 for i, aName := range r { 200 if aName == matchAll { 201 return true 202 } 203 if aName == matchSubject && b[i] == subjectUsername { 204 continue 205 } 206 if b[i] != aName { 207 return false 208 } 209 } 210 211 return len(r) == len(b) 212 } 213 214 // ResourceStrFromRef takes a dsref.Ref and returns a string that can be parsed 215 // as a resource 216 func ResourceStrFromRef(ref dsref.Ref) string { 217 return strings.Join([]string{"dataset", ref.Username, ref.Name}, ":") 218 } 219 220 // Actions is a slice of Action 221 type Actions []Action 222 223 // Contains determines if the given action is contained by the Actions 224 func (as Actions) Contains(b Action) bool { 225 for _, a := range as { 226 if a.Contains(b) { 227 return true 228 } 229 } 230 return false 231 } 232 233 // Action is a description of the action the Subject is attempting to take on 234 // the Resource 235 type Action []string 236 237 // MustParseAction parses a string into an Action. It panics if the string 238 // cannot be parsed correctly 239 func MustParseAction(str string) Action { 240 rsc, err := ParseAction(str) 241 if err != nil { 242 panic(err) 243 } 244 return rsc 245 } 246 247 // ParseAction parses a string into an Action 248 func ParseAction(str string) (Action, error) { 249 if str == "" { 250 return nil, fmt.Errorf("action string cannot be empty") 251 } 252 253 rsc := strings.Split(str, ":") 254 255 foundStar := false 256 for _, name := range rsc { 257 if name == matchAll { 258 if foundStar { 259 return nil, fmt.Errorf(`invalid action string %q. '*' character cannot occur twice`, str) 260 } 261 foundStar = true 262 } else if foundStar { 263 return nil, fmt.Errorf(`invalid action string %q. '*' must come last`, str) 264 } 265 } 266 267 return rsc, nil 268 } 269 270 // MarshalJSON marshals the Action into a string separated by ":" 271 func (a Action) MarshalJSON() ([]byte, error) { 272 return []byte(strings.Join(a, ":")), nil 273 } 274 275 // UnmarshalJSON unmarshals the given slice of bytes into an Action 276 func (a *Action) UnmarshalJSON(data []byte) error { 277 var str string 278 if err := json.Unmarshal(data, &str); err != nil { 279 return err 280 } 281 282 act, err := ParseAction(str) 283 if err != nil { 284 return err 285 } 286 287 *a = act 288 return nil 289 } 290 291 // Contains determines if the given action is described in the rule's Action 292 // it returns true if the action matches using the glob `*` pattern 293 func (a Action) Contains(b Action) bool { 294 if len(a) > len(b) { 295 return false 296 } 297 298 for i, aName := range a { 299 if aName == matchAll { 300 return true 301 } 302 if b[i] != aName { 303 return false 304 } 305 } 306 307 return len(a) == len(b) 308 }