storj/pkg/macaroon/apikey.go
Jeff Wendling 15e74c8c3d uplink share subcommand (#1924)
* cmd/uplink: add share command to restrict an api key

This commit is an early bit of work to just implement restricting
macaroon api keys from the command line. It does not convert
api keys to be macaroons in general.

It also does not apply the path restriction caveats appropriately
yet because it does not encrypt them.

* cmd/uplink: fix path encryption for shares

It should now properly encrypt the path prefixes when adding
caveats to a macaroon.

* fix up linting problems

* print summary of caveat and require iso8601

* make clone part more clear
2019-05-14 12:15:12 -06:00

193 lines
4.8 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package macaroon
import (
"bytes"
"time"
"github.com/btcsuite/btcutil/base58"
"github.com/gogo/protobuf/proto"
"github.com/zeebo/errs"
)
var (
// Error is a general API Key error
Error = errs.Class("api key error")
// ErrFormat means that the structural formatting of the API Key is invalid
ErrFormat = errs.Class("api key format error")
// ErrInvalid means that the API Key is improperly signed
ErrInvalid = errs.Class("api key invalid error")
// ErrUnauthorized means that the API key does not grant the requested permission
ErrUnauthorized = errs.Class("api key unauthorized error")
// ErrRevoked means the API key has been revoked
ErrRevoked = errs.Class("api key revocation error")
)
// ActionType specifies the operation type being performed that the Macaroon will validate
type ActionType int
const (
_ ActionType = iota // ActionType zero value
// ActionRead specifies a read operation
ActionRead
// ActionWrite specifies a read operation
ActionWrite
// ActionList specifies a read operation
ActionList
// ActionDelete specifies a read operation
ActionDelete
)
// Action specifies the specific operation being performed that the Macaroon will validate
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
}
// NewAPIKey generates a brand new unrestricted API key given the provided
// server project secret
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(secret []byte, action Action, revoked [][]byte) error {
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 := proto.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
}
// Restrict generates a new APIKey with the provided Caveat attached.
func (a *APIKey) Restrict(caveat Caveat) (*APIKey, error) {
buf, err := proto.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
func (a *APIKey) Serialize() string {
return base58.CheckEncode(a.mac.Serialize(), 0)
}
// Allows returns true if the provided action is allowed by the caveat.
func (c *Caveat) Allows(action Action) bool {
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
}
default:
return false
}
// 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
}
if len(c.AllowedPaths) > 0 {
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
}