1// Copyright (C) 2020 Storj Labs, Inc.
2// See LICENSE for copying information.
3
4package uplink
5
6import (
7	"context"
8	"errors"
9	"strings"
10	"time"
11
12	"github.com/zeebo/errs"
13
14	"storj.io/common/encryption"
15	"storj.io/common/macaroon"
16	"storj.io/common/paths"
17	"storj.io/common/rpc"
18	"storj.io/common/storj"
19	"storj.io/uplink/private/access2"
20	"storj.io/uplink/private/metainfo"
21)
22
23// An Access Grant contains everything to access a project and specific buckets.
24// It includes a potentially-restricted API Key, a potentially-restricted set
25// of encryption information, and information about the Satellite responsible
26// for the project's metadata.
27type Access struct {
28	satelliteURL storj.NodeURL
29	apiKey       *macaroon.APIKey
30	encAccess    *access2.EncryptionAccess
31}
32
33// getAPIKey are exposing the state do private methods.
34//
35// NB: this is used with linkname in internal/expose.
36// It needs to be updated when this is updated.
37//
38//lint:ignore U1000, used with linkname
39//nolint: unused
40func (access *Access) getAPIKey() *macaroon.APIKey { return access.apiKey }
41
42// getEncAccess are exposing the state do private methods.
43//
44// NB: this is used with linkname in internal/expose.
45// It needs to be updated when this is updated.
46//
47//lint:ignore U1000, used with linkname
48//nolint: unused
49func (access *Access) getEncAccess() *access2.EncryptionAccess { return access.encAccess }
50
51// SharePrefix defines a prefix that will be shared.
52type SharePrefix struct {
53	Bucket string
54	// Prefix is the prefix of the shared object keys.
55	//
56	// Note: that within a bucket, the hierarchical key derivation scheme is
57	// delineated by forward slashes (/), so encryption information will be
58	// included in the resulting access grant to decrypt any key that shares
59	// the same prefix up until the last slash.
60	Prefix string
61}
62
63// Permission defines what actions can be used to share.
64type Permission struct {
65	// AllowDownload gives permission to download the object's content. It
66	// allows getting object metadata, but it does not allow listing buckets.
67	AllowDownload bool
68	// AllowUpload gives permission to create buckets and upload new objects.
69	// It does not allow overwriting existing objects unless AllowDelete is
70	// granted too.
71	AllowUpload bool
72	// AllowList gives permission to list buckets. It allows getting object
73	// metadata, but it does not allow downloading the object's content.
74	AllowList bool
75	// AllowDelete gives permission to delete buckets and objects. Unless
76	// either AllowDownload or AllowList is granted too, no object metadata and
77	// no error info will be returned for deleted objects.
78	AllowDelete bool
79	// NotBefore restricts when the resulting access grant is valid for.
80	// If set, the resulting access grant will not work if the Satellite
81	// believes the time is before NotBefore.
82	// If set, this value should always be before NotAfter.
83	NotBefore time.Time
84	// NotAfter restricts when the resulting access grant is valid for.
85	// If set, the resulting access grant will not work if the Satellite
86	// believes the time is after NotAfter.
87	// If set, this value should always be after NotBefore.
88	NotAfter time.Time
89}
90
91// ParseAccess parses a serialized access grant string.
92//
93// This should be the main way to instantiate an access grant for opening a project.
94// See the note on RequestAccessWithPassphrase.
95func ParseAccess(access string) (*Access, error) {
96	inner, err := access2.ParseAccess(access)
97	if err != nil {
98		return nil, packageError.Wrap(err)
99	}
100
101	satelliteURL, err := parseNodeURL(inner.SatelliteAddress)
102	if err != nil {
103		return nil, packageError.Wrap(err)
104	}
105
106	return &Access{
107		satelliteURL: satelliteURL,
108		apiKey:       inner.APIKey,
109		encAccess:    inner.EncAccess,
110	}, nil
111}
112
113// SatelliteAddress returns the satellite node URL for this access grant.
114func (access *Access) SatelliteAddress() string {
115	return access.satelliteURL.String()
116}
117
118// Serialize serializes an access grant such that it can be used later with
119// ParseAccess or other tools.
120func (access *Access) Serialize() (string, error) {
121	inner := access2.Access{
122		SatelliteAddress: access.satelliteURL.String(),
123		APIKey:           access.apiKey,
124		EncAccess:        access.encAccess,
125	}
126	return inner.Serialize()
127}
128
129// RequestAccessWithPassphrase generates a new access grant using a passhprase.
130// It must talk to the Satellite provided to get a project-based salt for
131// deterministic key derivation.
132//
133// Note: this is a CPU-heavy function that uses a password-based key derivation function
134// (Argon2). This should be a setup-only step. Most common interactions with the library
135// should be using a serialized access grant through ParseAccess directly.
136func RequestAccessWithPassphrase(ctx context.Context, satelliteAddress, apiKey, passphrase string) (*Access, error) {
137	return (Config{}).RequestAccessWithPassphrase(ctx, satelliteAddress, apiKey, passphrase)
138}
139
140// RequestAccessWithPassphrase generates a new access grant using a passhprase.
141// It must talk to the Satellite provided to get a project-based salt for
142// deterministic key derivation.
143//
144// Note: this is a CPU-heavy function that uses a password-based key derivation function
145// (Argon2). This should be a setup-only step. Most common interactions with the library
146// should be using a serialized access grant through ParseAccess directly.
147func (config Config) RequestAccessWithPassphrase(ctx context.Context, satelliteAddress, apiKey, passphrase string) (*Access, error) {
148	return config.requestAccessWithPassphraseAndConcurrency(ctx, satelliteAddress, apiKey, passphrase, 8)
149}
150
151// requestAccessWithPassphraseAndConcurrency requests satellite for a new access grant using a passhprase and specific concurrency for the Argon2 key derivation.
152//
153// NB: this is used with linkname in internal/expose.
154// It needs to be updated when this is updated.
155func (config Config) requestAccessWithPassphraseAndConcurrency(ctx context.Context, satelliteAddress, apiKey, passphrase string, concurrency uint8) (_ *Access, err error) {
156	parsedAPIKey, err := macaroon.ParseAPIKey(apiKey)
157	if err != nil {
158		return nil, packageError.Wrap(err)
159	}
160
161	satelliteURL, err := parseNodeURL(satelliteAddress)
162	if err != nil {
163		return nil, packageError.Wrap(err)
164	}
165
166	dialer, err := config.getDialer(ctx)
167	if err != nil {
168		return nil, packageError.Wrap(err)
169	}
170	defer func() { err = errs.Combine(err, dialer.Pool.Close()) }()
171
172	metainfo, err := metainfo.DialNodeURL(ctx, dialer, satelliteURL.String(), parsedAPIKey, config.UserAgent)
173	if err != nil {
174		return nil, packageError.Wrap(err)
175	}
176	defer func() { err = errs.Combine(err, metainfo.Close()) }()
177
178	info, err := metainfo.GetProjectInfo(ctx)
179	if err != nil {
180		return nil, convertKnownErrors(err, "", "")
181	}
182
183	key, err := encryption.DeriveRootKey([]byte(passphrase), info.ProjectSalt, "", concurrency)
184	if err != nil {
185		return nil, packageError.Wrap(err)
186	}
187
188	encAccess := access2.NewEncryptionAccessWithDefaultKey(key)
189	encAccess.SetDefaultPathCipher(storj.EncAESGCM)
190	encAccess.LimitTo(parsedAPIKey)
191
192	return &Access{
193		satelliteURL: satelliteURL,
194		apiKey:       parsedAPIKey,
195		encAccess:    encAccess,
196	}, nil
197}
198
199// parseNodeURL parses the address into a storj.NodeURL adding the node id if necessary
200// for known addresses.
201func parseNodeURL(address string) (storj.NodeURL, error) {
202	nodeURL, err := storj.ParseNodeURL(address)
203	if err != nil {
204		return nodeURL, packageError.Wrap(err)
205	}
206
207	// Node id is required in satelliteNodeID for all unknown (non-storj) satellites.
208	// For known satellite it will be automatically prepended.
209	if nodeURL.ID.IsZero() {
210		nodeID, found := rpc.KnownNodeID(nodeURL.Address)
211		if !found {
212			return nodeURL, packageError.New("node id is required in satelliteNodeURL")
213		}
214		nodeURL.ID = nodeID
215	}
216
217	return nodeURL, nil
218}
219
220// Share creates a new access grant with specific permissions.
221//
222// Access grants can only have their existing permissions restricted,
223// and the resulting access grant will only allow for the intersection of all previous
224// Share calls in the access grant construction chain.
225//
226// Prefixes, if provided, restrict the access grant (and internal encryption information)
227// to only contain enough information to allow access to just those prefixes.
228//
229// To revoke an access grant see the Project.RevokeAccess method.
230func (access *Access) Share(permission Permission, prefixes ...SharePrefix) (*Access, error) {
231	if permission == (Permission{}) {
232		return nil, packageError.New("permission is empty")
233	}
234
235	var notBefore, notAfter *time.Time
236	if !permission.NotBefore.IsZero() {
237		notBefore = &permission.NotBefore
238	}
239	if !permission.NotAfter.IsZero() {
240		notAfter = &permission.NotAfter
241	}
242
243	if notBefore != nil && notAfter != nil && notAfter.Before(*notBefore) {
244		return nil, packageError.New("invalid time range")
245	}
246
247	caveat := macaroon.WithNonce(macaroon.Caveat{
248		DisallowReads:   !permission.AllowDownload,
249		DisallowWrites:  !permission.AllowUpload,
250		DisallowLists:   !permission.AllowList,
251		DisallowDeletes: !permission.AllowDelete,
252		NotBefore:       notBefore,
253		NotAfter:        notAfter,
254	})
255
256	sharedAccess := access2.NewEncryptionAccess()
257	sharedAccess.SetDefaultPathCipher(access.encAccess.Store.GetDefaultPathCipher())
258	if len(prefixes) == 0 {
259		sharedAccess.SetDefaultKey(access.encAccess.Store.GetDefaultKey())
260	}
261
262	for _, prefix := range prefixes {
263		// If the share prefix ends in a `/` we need to remove this final slash.
264		// Otherwise, if we the shared prefix is `/bob/`, the encrypted shared
265		// prefix results in `enc("")/enc("bob")/enc("")`. This is an incorrect
266		// encrypted prefix, what we really want is `enc("")/enc("bob")`.
267		unencPath := paths.NewUnencrypted(strings.TrimSuffix(prefix.Prefix, "/"))
268
269		encPath, err := encryption.EncryptPathWithStoreCipher(prefix.Bucket, unencPath, access.encAccess.Store)
270		if err != nil {
271			return nil, err
272		}
273		derivedKey, err := encryption.DerivePathKey(prefix.Bucket, unencPath, access.encAccess.Store)
274		if err != nil {
275			return nil, err
276		}
277
278		if err := sharedAccess.Store.Add(prefix.Bucket, unencPath, encPath, *derivedKey); err != nil {
279			return nil, err
280		}
281		caveat.AllowedPaths = append(caveat.AllowedPaths, &macaroon.Caveat_Path{
282			Bucket:              []byte(prefix.Bucket),
283			EncryptedPathPrefix: []byte(encPath.Raw()),
284		})
285	}
286
287	restrictedAPIKey, err := access.apiKey.Restrict(caveat)
288	if err != nil {
289		return nil, err
290	}
291
292	restrictedAccess := &Access{
293		satelliteURL: access.satelliteURL,
294		apiKey:       restrictedAPIKey,
295		encAccess:    sharedAccess,
296	}
297	return restrictedAccess, nil
298}
299
300// RevokeAccess revokes the API key embedded in the provided access grant.
301//
302// When an access grant is revoked, it will also revoke any further-restricted
303// access grants created (via the Access.Share method) from the revoked access
304// grant.
305//
306// An access grant is authorized to revoke any further-restricted access grant
307// created from it. An access grant cannot revoke itself. An unauthorized
308// request will return an error.
309//
310// There may be a delay between a successful revocation request and actual
311// revocation, depending on the satellite's access caching policies.
312func (project *Project) RevokeAccess(ctx context.Context, access *Access) (err error) {
313	defer mon.Task()(&ctx)(&err)
314
315	metainfoClient, err := project.dialMetainfoClient(ctx)
316	if err != nil {
317		return err
318	}
319	defer func() { err = errs.Combine(err, metainfoClient.Close()) }()
320
321	err = metainfoClient.RevokeAPIKey(ctx, metainfo.RevokeAPIKeyParams{
322		APIKey: access.apiKey.SerializeRaw(),
323	})
324	return convertKnownErrors(err, "", "")
325}
326
327// ReadOnlyPermission returns a Permission that allows reading and listing
328// (if the parent access grant already allows those things).
329func ReadOnlyPermission() Permission {
330	return Permission{
331		AllowDownload: true,
332		AllowList:     true,
333	}
334}
335
336// WriteOnlyPermission returns a Permission that allows writing and deleting
337// (if the parent access grant already allows those things).
338func WriteOnlyPermission() Permission {
339	return Permission{
340		AllowUpload: true,
341		AllowDelete: true,
342	}
343}
344
345// FullPermission returns a Permission that allows all actions that the
346// parent access grant already allows.
347func FullPermission() Permission {
348	return Permission{
349		AllowDownload: true,
350		AllowUpload:   true,
351		AllowList:     true,
352		AllowDelete:   true,
353	}
354}
355
356// OverrideEncryptionKey overrides the root encryption key for the prefix in
357// bucket with encryptionKey.
358//
359// This function is useful for overriding the encryption key in user-specific
360// access grants when implementing multitenancy in a single app bucket.
361// See the relevant section in the package documentation.
362func (access *Access) OverrideEncryptionKey(bucket, prefix string, encryptionKey *EncryptionKey) error {
363	if !strings.HasSuffix(prefix, "/") {
364		return errors.New("prefix must end with slash")
365	}
366
367	// We need to remove the trailing slash. Otherwise, if we the shared
368	// prefix is `/bob/`, the encrypted shared prefix results in
369	// `enc("")/enc("bob")/enc("")`. This is an incorrect encrypted prefix,
370	// what we really want is `enc("")/enc("bob")`.
371	prefix = strings.TrimSuffix(prefix, "/")
372
373	store := access.encAccess.Store
374
375	unencPath := paths.NewUnencrypted(prefix)
376	encPath, err := encryption.EncryptPathWithStoreCipher(bucket, unencPath, store)
377	if err != nil {
378		return convertKnownErrors(err, bucket, prefix)
379	}
380
381	err = store.Add(bucket, unencPath, encPath, *encryptionKey.key)
382	return convertKnownErrors(err, bucket, prefix)
383}
384