220 lines
6.0 KiB
Go
220 lines
6.0 KiB
Go
|
// Copyright (C) 2022 Storj Labs, Inc.
|
||
|
// See LICENSE for copying information.
|
||
|
|
||
|
package oidc
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"database/sql"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/go-oauth2/oauth2/v4"
|
||
|
|
||
|
"storj.io/common/macaroon"
|
||
|
"storj.io/common/uuid"
|
||
|
"storj.io/storj/satellite/console"
|
||
|
"storj.io/storj/satellite/console/consoleauth"
|
||
|
)
|
||
|
|
||
|
// UUIDAuthorizeGenerate generates an auth code using Storj's uuid.
|
||
|
type UUIDAuthorizeGenerate struct{}
|
||
|
|
||
|
// Token returns a new authorization code.
|
||
|
func (a *UUIDAuthorizeGenerate) Token(ctx context.Context, data *oauth2.GenerateBasic) (string, error) {
|
||
|
code, err := uuid.New()
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
return code.String(), nil
|
||
|
}
|
||
|
|
||
|
// MacaroonAccessGenerate provides an access_token and refresh_token generator using Storj's Macaroons.
|
||
|
type MacaroonAccessGenerate struct {
|
||
|
Service GenerateService
|
||
|
}
|
||
|
|
||
|
// GenerateService defines the minimal interface needed to generate macaroon based api keys.
|
||
|
type GenerateService interface {
|
||
|
GetAPIKeyInfoByName(context.Context, uuid.UUID, string) (*console.APIKeyInfo, error)
|
||
|
CreateAPIKey(context.Context, uuid.UUID, string) (*console.APIKeyInfo, *macaroon.APIKey, error)
|
||
|
GetUser(ctx context.Context, id uuid.UUID) (u *console.User, err error)
|
||
|
}
|
||
|
|
||
|
func (a *MacaroonAccessGenerate) apiKeyForProject(ctx context.Context, data *oauth2.GenerateBasic, project string) (*macaroon.APIKey, error) {
|
||
|
userID, err := uuid.FromString(data.UserID)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
projectID, err := uuid.FromString(project)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
user, err := a.Service.GetUser(ctx, userID)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
ctx = console.WithAuth(ctx, console.Authorization{
|
||
|
User: *user,
|
||
|
Claims: consoleauth.Claims{
|
||
|
ID: user.ID,
|
||
|
Email: user.Email,
|
||
|
},
|
||
|
})
|
||
|
|
||
|
oauthClient := data.Client.(OAuthClient)
|
||
|
name := oauthClient.AppName + " / " + oauthClient.ID.String()
|
||
|
|
||
|
var key *macaroon.APIKey
|
||
|
|
||
|
apiKeyInfo, err := a.Service.GetAPIKeyInfoByName(ctx, projectID, name)
|
||
|
if err == nil {
|
||
|
key, err = macaroon.FromParts(apiKeyInfo.Head, apiKeyInfo.Secret)
|
||
|
} else if errors.Is(err, sql.ErrNoRows) {
|
||
|
_, key, err = a.Service.CreateAPIKey(ctx, projectID, name)
|
||
|
}
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return key, nil
|
||
|
}
|
||
|
|
||
|
// Token issues access and refresh tokens that are backed by storj's Macaroons. This expects several scopes to be set on
|
||
|
// the request. The following describes the available scopes supported by the macaroon style of access token.
|
||
|
//
|
||
|
// project:<projectId> - required, scopes operations to a single project (one)
|
||
|
// bucket:<name> - optional, scopes operations to one or many buckets (repeatable)
|
||
|
// object:list - optional, allows listing object data
|
||
|
// object:read - optional, allows reading object data
|
||
|
// object:write - optional, allows writing object data
|
||
|
// object:delete - optional, allows deleting object data
|
||
|
//
|
||
|
// In OAuth2.0, access_tokens are short-lived tokens that authorize operations to be performed on behalf of an end user.
|
||
|
// refresh_tokens are longer lived tokens that allow you to obtain new authorization tokens.
|
||
|
func (a *MacaroonAccessGenerate) Token(ctx context.Context, data *oauth2.GenerateBasic, isGenRefresh bool) (access, refresh string, err error) {
|
||
|
var apiKey *macaroon.APIKey
|
||
|
|
||
|
if priorRefresh := data.TokenInfo.GetRefresh(); isGenRefresh && priorRefresh != "" {
|
||
|
apiKey, err = macaroon.ParseAPIKey(priorRefresh)
|
||
|
if err != nil {
|
||
|
return access, refresh, err
|
||
|
}
|
||
|
|
||
|
refresh = priorRefresh
|
||
|
} else {
|
||
|
info, perms, err := parseScope(data.TokenInfo.GetScope())
|
||
|
if err != nil {
|
||
|
return access, refresh, err
|
||
|
}
|
||
|
|
||
|
if info.Project == "" {
|
||
|
return access, refresh, fmt.Errorf("missing project")
|
||
|
}
|
||
|
|
||
|
apiKey, err = a.apiKeyForProject(ctx, data, info.Project)
|
||
|
if err != nil {
|
||
|
return access, refresh, err
|
||
|
}
|
||
|
|
||
|
apiKey, err = apiKey.Restrict(perms)
|
||
|
if err != nil {
|
||
|
return access, refresh, err
|
||
|
}
|
||
|
|
||
|
if isGenRefresh {
|
||
|
nonce, err := uuid.New()
|
||
|
if err != nil {
|
||
|
return "", "", err
|
||
|
}
|
||
|
|
||
|
createAt := data.TokenInfo.GetRefreshCreateAt()
|
||
|
expireAt := createAt.Add(data.TokenInfo.GetRefreshExpiresIn())
|
||
|
|
||
|
apiKey, err = apiKey.Restrict(macaroon.Caveat{
|
||
|
NotBefore: &(createAt),
|
||
|
NotAfter: &(expireAt),
|
||
|
Nonce: nonce.Bytes(),
|
||
|
})
|
||
|
|
||
|
if err != nil {
|
||
|
return access, refresh, err
|
||
|
}
|
||
|
|
||
|
refresh = apiKey.Serialize()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
nonce, err := uuid.New()
|
||
|
if err != nil {
|
||
|
return "", "", err
|
||
|
}
|
||
|
|
||
|
createAt := data.TokenInfo.GetAccessCreateAt()
|
||
|
expireAt := createAt.Add(data.TokenInfo.GetAccessExpiresIn())
|
||
|
|
||
|
apiKey, err = apiKey.Restrict(macaroon.Caveat{
|
||
|
NotBefore: &(createAt),
|
||
|
NotAfter: &(expireAt),
|
||
|
Nonce: nonce.Bytes(),
|
||
|
})
|
||
|
|
||
|
if err != nil {
|
||
|
return "", "", err
|
||
|
}
|
||
|
|
||
|
access = apiKey.Serialize()
|
||
|
return access, refresh, nil
|
||
|
}
|
||
|
|
||
|
func parseScope(scope string) (UserInfo, macaroon.Caveat, error) {
|
||
|
scopes := strings.Split(scope, " ")
|
||
|
|
||
|
info := UserInfo{}
|
||
|
perms := macaroon.Caveat{
|
||
|
DisallowLists: true,
|
||
|
DisallowReads: true,
|
||
|
DisallowWrites: true,
|
||
|
DisallowDeletes: true,
|
||
|
AllowedPaths: make([]*macaroon.Caveat_Path, 0, len(scopes)),
|
||
|
}
|
||
|
|
||
|
for i := 0; i < len(scopes); i++ {
|
||
|
scopes[i] = strings.TrimSpace(scopes[i])
|
||
|
|
||
|
switch {
|
||
|
case strings.HasPrefix(scopes[i], "project:"):
|
||
|
if info.Project != "" {
|
||
|
return info, perms, fmt.Errorf("multiple project scopes provided")
|
||
|
}
|
||
|
|
||
|
info.Project = strings.TrimPrefix(scopes[i], "project:")
|
||
|
case strings.HasPrefix(scopes[i], "bucket:"):
|
||
|
bucket := strings.TrimPrefix(scopes[i], "bucket:")
|
||
|
info.Buckets = append(info.Buckets, bucket)
|
||
|
|
||
|
perms.AllowedPaths = append(perms.AllowedPaths, &macaroon.Caveat_Path{
|
||
|
Bucket: []byte(bucket),
|
||
|
})
|
||
|
case strings.HasPrefix(scopes[i], "cubbyhole:"):
|
||
|
info.Cubbyhole = strings.TrimPrefix(scopes[i], "cubbyhole:")
|
||
|
case scopes[i] == "object:list":
|
||
|
perms.DisallowLists = false
|
||
|
case scopes[i] == "object:read":
|
||
|
perms.DisallowReads = false
|
||
|
case scopes[i] == "object:write":
|
||
|
perms.DisallowWrites = false
|
||
|
case scopes[i] == "object:delete":
|
||
|
perms.DisallowDeletes = false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return info, perms, nil
|
||
|
}
|