ed5ebb2527
"REST API" is a more accurate descriptor of the generated API in the console package than "account management API". The generated API is very flexible and will allow us to implement many more endpoints outside the scope of "account management", and "account management" is not very well defined to begin with. Change-Id: Ie87faeaa3c743ef4371eaf0edd2826303d592da7
176 lines
5.0 KiB
Go
176 lines
5.0 KiB
Go
// Copyright (C) 2022 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package restkeys
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/spacemonkeygo/monkit/v3"
|
|
"github.com/zeebo/errs"
|
|
|
|
"storj.io/common/uuid"
|
|
"storj.io/storj/satellite/oidc"
|
|
)
|
|
|
|
var mon = monkit.Package()
|
|
|
|
var (
|
|
// Error describes internal rest keys error.
|
|
Error = errs.Class("rest keys service")
|
|
|
|
// ErrDuplicateKey is error type that occurs when a generated account
|
|
// management api key already exists.
|
|
ErrDuplicateKey = errs.Class("duplicate key")
|
|
|
|
// ErrInvalidKey is an error type that occurs when a user submits a key
|
|
// that does not match anything in the database.
|
|
ErrInvalidKey = errs.Class("invalid key")
|
|
)
|
|
|
|
// Config contains configuration parameters for rest keys.
|
|
type Config struct {
|
|
DefaultExpiration time.Duration `help:"expiration to use if user does not specify an rest key expiration" default:"720h"`
|
|
}
|
|
|
|
// Service handles operations regarding rest keys.
|
|
type Service struct {
|
|
db oidc.OAuthTokens
|
|
config Config
|
|
}
|
|
|
|
// NewService creates a new rest keys service.
|
|
func NewService(db oidc.OAuthTokens, config Config) *Service {
|
|
return &Service{
|
|
db: db,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
// Create creates and inserts an rest key into the db.
|
|
func (s *Service) Create(ctx context.Context, userID uuid.UUID, expiration time.Duration) (apiKey string, expiresAt time.Time, err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
apiKey, hash, err := s.GenerateNewKey(ctx)
|
|
if err != nil {
|
|
return "", time.Time{}, Error.Wrap(err)
|
|
}
|
|
expiresAt, err = s.InsertIntoDB(ctx, oidc.OAuthToken{
|
|
UserID: userID,
|
|
Kind: oidc.KindRESTTokenV0,
|
|
Token: hash,
|
|
}, time.Now(), expiration)
|
|
if err != nil {
|
|
return "", time.Time{}, Error.Wrap(err)
|
|
}
|
|
return apiKey, expiresAt, nil
|
|
}
|
|
|
|
// GenerateNewKey generates a new account management api key.
|
|
func (s *Service) GenerateNewKey(ctx context.Context) (apiKey, hash string, err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
id, err := uuid.New()
|
|
if err != nil {
|
|
return "", "", Error.Wrap(err)
|
|
}
|
|
|
|
apiKey = id.String()
|
|
hash = hashKeyFromUUID(ctx, id)
|
|
return apiKey, hash, nil
|
|
}
|
|
|
|
// This is used for hashing during key creation so we don't need to convert from a string back to a uuid.
|
|
func hashKeyFromUUID(ctx context.Context, apiKeyUUID uuid.UUID) string {
|
|
mon.Task()(&ctx)(nil)
|
|
|
|
hashBytes := sha256.Sum256(apiKeyUUID.Bytes())
|
|
return string(hashBytes[:])
|
|
}
|
|
|
|
// HashKey returns a hash of api key. This is used for hashing inside GetUserFromKey.
|
|
func (s *Service) HashKey(ctx context.Context, apiKey string) (hash string, err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
id, err := uuid.FromString(apiKey)
|
|
if err != nil {
|
|
return "", Error.Wrap(err)
|
|
}
|
|
hashBytes := sha256.Sum256(id.Bytes())
|
|
return string(hashBytes[:]), nil
|
|
}
|
|
|
|
// InsertIntoDB checks OAuthTokens DB for a token before inserting. This is because OAuthTokens DB allows
|
|
// duplicate tokens, but we can't have duplicate api keys.
|
|
func (s *Service) InsertIntoDB(ctx context.Context, oAuthToken oidc.OAuthToken, now time.Time, expiration time.Duration) (expiresAt time.Time, err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
// The token column is the key to the OAuthTokens table, but the Create method does not return an error if a duplicate token insert is attempted.
|
|
// We need to make sure a unique api key is created, so check that the value doesn't already exist.
|
|
_, err = s.db.Get(ctx, oidc.KindRESTTokenV0, oAuthToken.Token)
|
|
if err != nil {
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
return time.Time{}, Error.Wrap(err)
|
|
}
|
|
} else if err == nil {
|
|
return time.Time{}, Error.Wrap(ErrDuplicateKey.New("failed to generate a unique account management api key"))
|
|
}
|
|
|
|
if expiration <= 0 {
|
|
expiration = s.config.DefaultExpiration
|
|
}
|
|
|
|
expiresAt = now.Add(expiration)
|
|
|
|
oAuthToken.CreatedAt = now
|
|
oAuthToken.ExpiresAt = expiresAt
|
|
|
|
err = s.db.Create(ctx, oAuthToken)
|
|
if err != nil {
|
|
return time.Time{}, Error.Wrap(err)
|
|
}
|
|
return expiresAt, nil
|
|
}
|
|
|
|
// GetUserAndExpirationFromKey gets the userID and expiration date attached to an account management api key.
|
|
func (s *Service) GetUserAndExpirationFromKey(ctx context.Context, apiKey string) (userID uuid.UUID, exp time.Time, err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
hash, err := s.HashKey(ctx, apiKey)
|
|
if err != nil {
|
|
return uuid.UUID{}, time.Now(), err
|
|
}
|
|
keyInfo, err := s.db.Get(ctx, oidc.KindRESTTokenV0, hash)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return uuid.UUID{}, time.Now(), Error.Wrap(ErrInvalidKey.New("invalid account management api key"))
|
|
}
|
|
return uuid.UUID{}, time.Now(), err
|
|
}
|
|
return keyInfo.UserID, keyInfo.ExpiresAt, err
|
|
}
|
|
|
|
// Revoke revokes an account management api key.
|
|
func (s *Service) Revoke(ctx context.Context, apiKey string) (err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
hash, err := s.HashKey(ctx, apiKey)
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
|
|
_, err = s.db.Get(ctx, oidc.KindRESTTokenV0, hash)
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
err = s.db.RevokeRESTTokenV0(ctx, hash)
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|