rclone/vendor/storj.io/common/macaroon/apikey.go

295 lines
8.0 KiB
Go
Raw Normal View History

2020-05-11 20:57:46 +02:00
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package macaroon
import (
"bytes"
"context"
"time"
"github.com/btcsuite/btcutil/base58"
"github.com/spacemonkeygo/monkit/v3"
"github.com/zeebo/errs"
"storj.io/common/pb"
)
var (
// Error is a general API Key error.
2020-05-11 20:57:46 +02:00
Error = errs.Class("api key error")
// ErrFormat means that the structural formatting of the API Key is invalid.
2020-05-11 20:57:46 +02:00
ErrFormat = errs.Class("api key format error")
// ErrInvalid means that the API Key is improperly signed.
2020-05-11 20:57:46 +02:00
ErrInvalid = errs.Class("api key invalid error")
// ErrUnauthorized means that the API key does not grant the requested permission.
2020-05-11 20:57:46 +02:00
ErrUnauthorized = errs.Class("api key unauthorized error")
// ErrRevoked means the API key has been revoked.
2020-05-11 20:57:46 +02:00
ErrRevoked = errs.Class("api key revocation error")
mon = monkit.Package()
)
// ActionType specifies the operation type being performed that the Macaroon will validate.
2020-05-11 20:57:46 +02:00
type ActionType int
const (
// not using iota because these values are persisted in macaroons.
2020-05-11 20:57:46 +02:00
_ ActionType = 0
// ActionRead specifies a read operation.
2020-05-11 20:57:46 +02:00
ActionRead ActionType = 1
// ActionWrite specifies a read operation.
2020-05-11 20:57:46 +02:00
ActionWrite ActionType = 2
// ActionList specifies a read operation.
2020-05-11 20:57:46 +02:00
ActionList ActionType = 3
// ActionDelete specifies a read operation.
2020-05-11 20:57:46 +02:00
ActionDelete ActionType = 4
// ActionProjectInfo requests project-level information.
2020-05-11 20:57:46 +02:00
ActionProjectInfo ActionType = 5
)
// Action specifies the specific operation being performed that the Macaroon will validate.
2020-05-11 20:57:46 +02:00
type Action struct {
Op ActionType
Bucket []byte
EncryptedPath []byte
Time time.Time
}
// APIKey implements a Macaroon-backed Storj-v3 API key.
type APIKey struct {
mac *Macaroon
}
// ParseAPIKey parses a given api key string and returns an APIKey if the
// APIKey was correctly formatted. It does not validate the key.
func ParseAPIKey(key string) (*APIKey, error) {
data, version, err := base58.CheckDecode(key)
if err != nil || version != 0 {
return nil, ErrFormat.New("invalid api key format")
}
mac, err := ParseMacaroon(data)
if err != nil {
return nil, ErrFormat.Wrap(err)
}
return &APIKey{mac: mac}, nil
}
// ParseRawAPIKey parses raw api key data and returns an APIKey if the APIKey
// was correctly formatted. It does not validate the key.
func ParseRawAPIKey(data []byte) (*APIKey, error) {
mac, err := ParseMacaroon(data)
if err != nil {
return nil, ErrFormat.Wrap(err)
}
return &APIKey{mac: mac}, nil
}
// NewAPIKey generates a brand new unrestricted API key given the provided.
// server project secret.
2020-05-11 20:57:46 +02:00
func NewAPIKey(secret []byte) (*APIKey, error) {
mac, err := NewUnrestricted(secret)
if err != nil {
return nil, Error.Wrap(err)
}
return &APIKey{mac: mac}, nil
}
// Check makes sure that the key authorizes the provided action given the root
// project secret and any possible revocations, returning an error if the action
// is not authorized. 'revoked' is a list of revoked heads.
func (a *APIKey) Check(ctx context.Context, secret []byte, action Action, revoked [][]byte) (err error) {
defer mon.Task()(&ctx)(&err)
if !a.mac.Validate(secret) {
return ErrInvalid.New("macaroon unauthorized")
}
// a timestamp is always required on an action
if action.Time.IsZero() {
return Error.New("no timestamp provided")
}
caveats := a.mac.Caveats()
for _, cavbuf := range caveats {
var cav Caveat
err := pb.Unmarshal(cavbuf, &cav)
if err != nil {
return ErrFormat.New("invalid caveat format")
}
if !cav.Allows(action) {
return ErrUnauthorized.New("action disallowed")
}
}
head := a.mac.Head()
for _, revokedID := range revoked {
if bytes.Equal(revokedID, head) {
return ErrRevoked.New("macaroon head revoked")
}
}
return nil
}
// AllowedBuckets stores information about which buckets are
// allowed to be accessed, where `Buckets` stores names of buckets that are
// allowed and `All` is a bool that indicates if all buckets are allowed or not.
2020-05-11 20:57:46 +02:00
type AllowedBuckets struct {
All bool
Buckets map[string]struct{}
}
// GetAllowedBuckets returns a list of all the allowed bucket paths that match the Action operation.
2020-05-11 20:57:46 +02:00
func (a *APIKey) GetAllowedBuckets(ctx context.Context, action Action) (allowed AllowedBuckets, err error) {
defer mon.Task()(&ctx)(&err)
// Every bucket is allowed until we find a caveat that restricts some paths.
allowed.All = true
// every caveat that includes a list of allowed paths must include the bucket for
// the bucket to be allowed. in other words, the set of allowed buckets is the
// intersection of all of the buckets in the allowed paths.
for _, cavbuf := range a.mac.Caveats() {
var cav Caveat
err := pb.Unmarshal(cavbuf, &cav)
if err != nil {
return AllowedBuckets{}, ErrFormat.New("invalid caveat format: %v", err)
}
if !cav.Allows(action) {
return AllowedBuckets{}, ErrUnauthorized.New("action disallowed")
}
// If the caveat does not include any allowed paths, then it is not restricting it.
if len(cav.AllowedPaths) == 0 {
continue
}
// Since we found some path restrictions, it's definitely the case that not every
// bucket is allowed.
allowed.All = false
caveatBuckets := map[string]struct{}{}
for _, caveatPath := range cav.AllowedPaths {
caveatBuckets[string(caveatPath.Bucket)] = struct{}{}
}
if allowed.Buckets == nil {
allowed.Buckets = caveatBuckets
} else {
for bucket := range allowed.Buckets {
if _, ok := caveatBuckets[bucket]; !ok {
delete(allowed.Buckets, bucket)
}
}
}
}
return allowed, err
}
// Restrict generates a new APIKey with the provided Caveat attached.
func (a *APIKey) Restrict(caveat Caveat) (*APIKey, error) {
buf, err := pb.Marshal(&caveat)
if err != nil {
return nil, Error.Wrap(err)
}
mac, err := a.mac.AddFirstPartyCaveat(buf)
if err != nil {
return nil, Error.Wrap(err)
}
return &APIKey{mac: mac}, nil
}
// Head returns the identifier for this macaroon's root ancestor.
func (a *APIKey) Head() []byte {
return a.mac.Head()
}
// Tail returns the identifier for this macaroon only.
func (a *APIKey) Tail() []byte {
return a.mac.Tail()
}
// Serialize serializes the API Key to a string.
2020-05-11 20:57:46 +02:00
func (a *APIKey) Serialize() string {
return base58.CheckEncode(a.mac.Serialize(), 0)
}
// SerializeRaw serialize the API Key to raw bytes.
2020-05-11 20:57:46 +02:00
func (a *APIKey) SerializeRaw() []byte {
return a.mac.Serialize()
}
// Allows returns true if the provided action is allowed by the caveat.
func (c *Caveat) Allows(action Action) bool {
// if the action is after the caveat's "not after" field, then it is invalid
if c.NotAfter != nil && action.Time.After(*c.NotAfter) {
return false
}
// if the caveat's "not before" field is *after* the action, then the action
// is before the "not before" field and it is invalid
if c.NotBefore != nil && c.NotBefore.After(action.Time) {
return false
}
// we want to always allow reads for bucket metadata, perhaps filtered by the
// buckets in the allowed paths.
if action.Op == ActionRead && len(action.EncryptedPath) == 0 {
if len(c.AllowedPaths) == 0 {
return true
}
if len(action.Bucket) == 0 {
// if no action.bucket name is provided, then this call is checking that
// we can list all buckets. In that case, return true here and we will
// filter out buckets that aren't allowed later with `GetAllowedBuckets()`
return true
}
for _, path := range c.AllowedPaths {
if bytes.Equal(path.Bucket, action.Bucket) {
return true
}
}
return false
}
switch action.Op {
case ActionRead:
if c.DisallowReads {
return false
}
case ActionWrite:
if c.DisallowWrites {
return false
}
case ActionList:
if c.DisallowLists {
return false
}
case ActionDelete:
if c.DisallowDeletes {
return false
}
case ActionProjectInfo:
// allow
default:
return false
}
if len(c.AllowedPaths) > 0 && action.Op != ActionProjectInfo {
found := false
for _, path := range c.AllowedPaths {
if bytes.Equal(action.Bucket, path.Bucket) &&
bytes.HasPrefix(action.EncryptedPath, path.EncryptedPathPrefix) {
found = true
break
}
}
if !found {
return false
}
}
return true
}