File: authorization_objects.go

package info (click to toggle)
incus 6.0.5-7
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 25,788 kB
  • sloc: sh: 16,313; ansic: 3,121; python: 457; makefile: 337; ruby: 51; sql: 50; lisp: 6
file content (361 lines) | stat: -rw-r--r-- 13,366 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
package auth

import (
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"github.com/gorilla/mux"

	"github.com/lxc/incus/v6/internal/version"
)

// Object is a string alias that represents an authorization object. These are formatted strings that
// uniquely identify an API resource, and can be constructed/deconstructed reliably.
// An Object is always of the form <ObjectType>:<identifier> where the identifier is a "/" delimited path containing elements that
// uniquely identify a resource. If the resource is defined at the project level, the first element of this path is always the project.
// Some example objects would be:
//   - `instance:default/c1`: Instance object in project "default" and name "c1".
//   - `storage_pool:local`: Storage pool object with name "local".
//   - `storage_volume:default/local/custom/vol1`: Storage volume object in project "default", storage pool "local", type "custom", and name "vol1".
type Object string

const (
	// objectTypeDelimiter is the string which separates the ObjectType from the remaining elements. Object types are
	// statically defined and do not contain this character, so we can extract the object type from an object by splitting
	// the string at this character.
	objectTypeDelimiter = ":"

	// objectElementDelimiter is the string which separates the elements of an object that make it a uniquely identifiable
	// resource. This was chosen because the character is not allowed in the majority of Incus resource names. Nevertheless
	// it is still necessary to escape this character in order to reliably construct/deconstruct an Object.
	objectElementDelimiter = "/"
)

// String implements fmt.Stringer for Object.
func (o Object) String() string {
	return string(o)
}

// Type returns the ObjectType of the Object.
func (o Object) Type() ObjectType {
	t, _, _ := strings.Cut(o.String(), objectTypeDelimiter)
	return ObjectType(t)
}

// Project returns the project of the Object if present.
func (o Object) Project() string {
	project, _ := o.projectAndElements()
	return project
}

// Elements returns the elements that uniquely identify the authorization Object.
func (o Object) Elements() []string {
	_, elements := o.projectAndElements()
	return elements
}

func (o Object) projectAndElements() (string, []string) {
	validator := objectValidators[o.Type()]
	_, identifier, _ := strings.Cut(o.String(), objectTypeDelimiter)

	var projectName string
	escapedObjectComponents := strings.SplitN(identifier, objectElementDelimiter, -1)
	components := make([]string, 0, len(escapedObjectComponents))
	for i, escapedComponent := range escapedObjectComponents {
		if validator.requireProject && i == 0 {
			projectName = unescape(escapedComponent)
			continue
		}

		components = append(components, unescape(escapedComponent))
	}

	return projectName, components
}

func (o Object) validate() error {
	objectType := o.Type()
	v, ok := objectValidators[objectType]
	if !ok {
		return fmt.Errorf("Missing validator for object of type %q", objectType)
	}

	projectName, identifierElements := o.projectAndElements()
	if v.requireProject && projectName == "" {
		return fmt.Errorf("Authorization objects of type %q require a project", objectType)
	}

	if len(identifierElements) < v.minIdentifierElements {
		return fmt.Errorf("Authorization objects of type %q require at least %d components to be uniquely identifiable", objectType, v.minIdentifierElements)
	}

	if len(identifierElements) > v.maxIdentifierElements {
		return fmt.Errorf("Authorization objects of type %q require at most %d components to be uniquely identifiable", objectType, v.maxIdentifierElements)
	}

	return nil
}

// objectValidator contains fields that can be used to determine if a string is a valid Object.
type objectValidator struct {
	minIdentifierElements int
	maxIdentifierElements int
	requireProject        bool
}

var objectValidators = map[ObjectType]objectValidator{
	ObjectTypeUser:               {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: false},
	ObjectTypeServer:             {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: false},
	ObjectTypeCertificate:        {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: false},
	ObjectTypeStoragePool:        {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: false},
	ObjectTypeProject:            {minIdentifierElements: 0, maxIdentifierElements: 0, requireProject: true},
	ObjectTypeImage:              {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: true},
	ObjectTypeImageAlias:         {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: true},
	ObjectTypeInstance:           {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: true},
	ObjectTypeNetwork:            {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: true},
	ObjectTypeNetworkACL:         {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: true},
	ObjectTypeNetworkIntegration: {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: false},
	ObjectTypeNetworkZone:        {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: true},
	ObjectTypeProfile:            {minIdentifierElements: 1, maxIdentifierElements: 1, requireProject: true},
	ObjectTypeStorageBucket:      {minIdentifierElements: 2, maxIdentifierElements: 3, requireProject: true},
	ObjectTypeStorageVolume:      {minIdentifierElements: 3, maxIdentifierElements: 4, requireProject: true},
}

// NewObject returns an Object of the given type. The passed in arguments must be in the correct
// order (as found in the URL for the resource). This function will error if an invalid object type is
// given, or if the correct number of arguments is not passed in.
func NewObject(objectType ObjectType, projectName string, identifierElements ...string) (Object, error) {
	v, ok := objectValidators[objectType]
	if !ok {
		return "", fmt.Errorf("Missing validator for object of type %q", objectType)
	}

	if v.requireProject && projectName == "" {
		return "", fmt.Errorf("Authorization objects of type %q require a project", objectType)
	}

	if len(identifierElements) < v.minIdentifierElements {
		return "", fmt.Errorf("Authorization objects of type %q require at least %d components to be uniquely identifiable", objectType, v.minIdentifierElements)
	}

	if len(identifierElements) > v.maxIdentifierElements {
		return "", fmt.Errorf("Authorization objects of type %q require at most %d components to be uniquely identifiable", objectType, v.maxIdentifierElements)
	}

	builder := strings.Builder{}
	builder.WriteString(string(objectType))
	builder.WriteString(objectTypeDelimiter)
	if v.requireProject {
		builder.WriteString(escape(projectName))
		if len(identifierElements) > 0 {
			builder.WriteString(objectElementDelimiter)
		}
	}

	for i, c := range identifierElements {
		builder.WriteString(escape(c))
		if i != len(identifierElements)-1 {
			builder.WriteString(objectElementDelimiter)
		}
	}

	return Object(builder.String()), nil
}

// ObjectFromRequest returns an object created from the request by evaluating the given mux vars.
// Mux vars must be provided in the order that they are found in the endpoint path. If the object
// requires a project name, this is taken from the project query parameter unless the URL begins
// with /1.0/projects.
func ObjectFromRequest(r *http.Request, objectType ObjectType, expandProject func(string) string, expandFingerprint func(string, string) string, expandVolumeLocation func(string, string, string, string) string, muxVars ...string) (Object, error) {
	// Shortcut for server objects which don't require any arguments.
	if objectType == ObjectTypeServer {
		return ObjectServer(), nil
	}

	values, err := url.ParseQuery(r.URL.RawQuery)
	if err != nil {
		return "", err
	}

	projectName := values.Get("project")
	if projectName == "" {
		projectName = "default"
	} else if projectName != "default" {
		projectName = expandProject(projectName)
	}

	location := values.Get("target")

	muxValues := make([]string, 0, len(muxVars))
	vars := mux.Vars(r)
	for _, muxVar := range muxVars {
		var err error
		var muxValue string

		if muxVar == "location" {
			// Special handling for the location which is not present as a real mux var.
			if location != "" {
				muxValue = location
			} else if objectType == ObjectTypeStorageVolume {
				muxValue = expandVolumeLocation(projectName, vars["poolName"], vars["type"], vars["volumeName"])
			}

			if muxValue == "" {
				continue
			}
		} else {
			muxValue, err = url.PathUnescape(vars[muxVar])
			if err != nil {
				return "", fmt.Errorf("Failed to unescape mux var %q for object type %q: %w", muxVar, objectType, err)
			}

			if muxValue == "" {
				return "", fmt.Errorf("Mux var %q not found for object type %q", muxVar, objectType)
			}

			// Expand fingerprints.
			if muxVar == "fingerprint" {
				muxValue = expandFingerprint(projectName, muxValue)
			}
		}

		muxValues = append(muxValues, muxValue)
	}

	// If using projects API we want to pass in the mux var, not the query parameter.
	if objectType == ObjectTypeProject && strings.HasPrefix(r.URL.Path, fmt.Sprintf("/%s/projects", version.APIVersion)) {
		if len(muxValues) == 0 {
			return "", errors.New("Missing project name path variable")
		}

		return ObjectProject(muxValues[0]), nil
	}

	return NewObject(objectType, projectName, muxValues...)
}

// ObjectFromString parses a string into an Object. It returns an error if the string is not valid.
func ObjectFromString(objectstr string) (Object, error) {
	o := Object(objectstr)
	err := o.validate()
	if err != nil {
		return "", err
	}

	return o, nil
}

// ObjectUser represents a user.
func ObjectUser(userName string) Object {
	object, _ := NewObject(ObjectTypeUser, "", userName)
	return object
}

// ObjectServer represents a server.
func ObjectServer() Object {
	object, _ := NewObject(ObjectTypeServer, "", "incus")
	return object
}

// ObjectCertificate represents a certificate.
func ObjectCertificate(fingerprint string) Object {
	object, _ := NewObject(ObjectTypeCertificate, "", fingerprint)
	return object
}

// ObjectStoragePool represents a storage pool.
func ObjectStoragePool(storagePoolName string) Object {
	object, _ := NewObject(ObjectTypeStoragePool, "", storagePoolName)
	return object
}

// ObjectProject represents a project.
func ObjectProject(projectName string) Object {
	object, _ := NewObject(ObjectTypeProject, projectName)
	return object
}

// ObjectImage represents an image.
func ObjectImage(projectName string, imageFingerprint string) Object {
	object, _ := NewObject(ObjectTypeImage, projectName, imageFingerprint)
	return object
}

// ObjectImageAlias represents an image alias.
func ObjectImageAlias(projectName string, aliasName string) Object {
	object, _ := NewObject(ObjectTypeImageAlias, projectName, aliasName)
	return object
}

// ObjectInstance represents an instance.
func ObjectInstance(projectName string, instanceName string) Object {
	object, _ := NewObject(ObjectTypeInstance, projectName, instanceName)
	return object
}

// ObjectNetwork represents a network.
func ObjectNetwork(projectName string, networkName string) Object {
	object, _ := NewObject(ObjectTypeNetwork, projectName, networkName)
	return object
}

// ObjectNetworkACL represents a network ACL.
func ObjectNetworkACL(projectName string, networkACLName string) Object {
	object, _ := NewObject(ObjectTypeNetworkACL, projectName, networkACLName)
	return object
}

// ObjectNetworkIntegration represents a network integration.
func ObjectNetworkIntegration(networkIntegrationName string) Object {
	object, _ := NewObject(ObjectTypeNetworkIntegration, "", networkIntegrationName)
	return object
}

// ObjectNetworkZone represents a network zone.
func ObjectNetworkZone(projectName string, networkZoneName string) Object {
	object, _ := NewObject(ObjectTypeNetworkZone, projectName, networkZoneName)
	return object
}

// ObjectProfile represents a profile.
func ObjectProfile(projectName string, profileName string) Object {
	object, _ := NewObject(ObjectTypeProfile, projectName, profileName)
	return object
}

// ObjectStorageBucket represents a storage bucket.
func ObjectStorageBucket(projectName string, poolName string, bucketName string, location string) Object {
	var object Object
	if location != "" {
		object, _ = NewObject(ObjectTypeStorageBucket, projectName, poolName, bucketName, location)
	} else {
		object, _ = NewObject(ObjectTypeStorageBucket, projectName, poolName, bucketName)
	}

	return object
}

// ObjectStorageVolume represents a storage volume.
func ObjectStorageVolume(projectName string, poolName string, volumeType string, volumeName string, location string) Object {
	var object Object
	if location != "" {
		object, _ = NewObject(ObjectTypeStorageVolume, projectName, poolName, volumeType, volumeName, location)
	} else {
		object, _ = NewObject(ObjectTypeStorageVolume, projectName, poolName, volumeType, volumeName)
	}

	return object
}

// escape escapes only the forward slash character as this is used as a delimiter. Everything else is allowed.
func escape(s string) string {
	return strings.ReplaceAll(s, "/", "%2F")
}

// unescape replaces only the escaped forward slashes.
func unescape(s string) string {
	return strings.ReplaceAll(s, "%2F", "/")
}