Skip to content

Commit 6532073

Browse files
authored
Merge pull request #1435 from stgraber/openfga
Openfga improvements
2 parents 50aba9a + ab29ab8 commit 6532073

File tree

12 files changed

+108
-106
lines changed

12 files changed

+108
-106
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,4 +331,4 @@ tags: */*.go
331331
# OpenFGA Syntax Transformer: https://github.com/openfga/syntax-transformer
332332
.PHONY: update-openfga
333333
update-openfga:
334-
@printf 'package auth\n\n// Code generated by Makefile; DO NOT EDIT.\n\nvar authModel = `%s`\n' '$(shell npx --yes @openfga/syntax-transformer transform --from=dsl --inputFile=./internal/server/auth/driver_openfga_model.openfga | jq -c)' > ./internal/server/auth/driver_openfga_model.go
334+
@printf 'package auth\n\n// Code generated by Makefile; DO NOT EDIT.\n\nvar authModel = `%s`\n' '$(shell fga model transform --file=./internal/server/auth/driver_openfga_model.openfga | jq -c)' > ./internal/server/auth/driver_openfga_model.go

cmd/incusd/api_1.0.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ func api10Get(d *Daemon, r *http.Request) response.Response {
394394
fullSrv.AuthUserName = requestor.Username
395395
fullSrv.AuthUserMethod = requestor.Protocol
396396

397-
err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectServer(), auth.EntitlementCanEdit)
397+
err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectServer(), auth.EntitlementCanViewSensitive)
398398
if err == nil {
399399
fullSrv.Config = fullSrvConfig
400400
} else if !api.StatusErrorCheck(err, http.StatusForbidden) {

cmd/incusd/patches.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ var patches = []patch{
8585
{name: "storage_zfs_unset_invalid_block_settings_v2", stage: patchPostDaemonStorage, run: patchStorageZfsUnsetInvalidBlockSettingsV2},
8686
{name: "runtime_directory", stage: patchPostDaemonStorage, run: patchRuntimeDirectory},
8787
{name: "lvm_node_force_reuse", stage: patchPostDaemonStorage, run: patchLvmForceReuseKey},
88+
{name: "auth_openfga_viewer", stage: patchPostNetworks, run: patchGenericAuthorization},
8889
}
8990

9091
type patch struct {
@@ -135,6 +136,10 @@ func patchesApply(d *Daemon, stage patchStage) error {
135136
return fmt.Errorf("Patch %q has no stage set: %d", patch.name, patch.stage)
136137
}
137138

139+
if patch.stage != stage {
140+
continue
141+
}
142+
138143
if slices.Contains(appliedPatches, patch.name) {
139144
continue
140145
}
@@ -680,6 +685,29 @@ func patchMoveBackupsInstances(name string, d *Daemon) error {
680685
return nil
681686
}
682687

688+
func patchGenericAuthorization(name string, d *Daemon) error {
689+
// Only run authorization patches on the leader.
690+
isLeader := false
691+
692+
leaderAddress, err := d.gateway.LeaderAddress()
693+
if err != nil {
694+
if !errors.Is(err, cluster.ErrNodeIsNotClustered) {
695+
return err
696+
}
697+
698+
isLeader = true
699+
} else if leaderAddress == d.localConfig.ClusterAddress() {
700+
isLeader = true
701+
}
702+
703+
// If clustered and not running on a leader, skip the resource update.
704+
if !isLeader {
705+
return nil
706+
}
707+
708+
return d.authorizer.ApplyPatch(d.shutdownCtx, name)
709+
}
710+
683711
func patchGenericStorage(name string, d *Daemon) error {
684712
return storagePools.Patch(d.State(), name)
685713
}

doc/authorization.md

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,14 @@ Incus will connect to the OpenFGA server, write the {ref}`openfga-model`, and qu
3939
With OpenFGA, access to a particular API resource is determined by the user's relationship to it.
4040
These relationships are determined by an [OpenFGA authorization model](https://openfga.dev/docs/concepts#what-is-an-authorization-model).
4141
The Incus OpenFGA authorization model describes API resources in terms of their relationship to other resources, and a relationship a user or group might have with that resource.
42-
Some convenient relations have also been built into the model:
43-
44-
- `server -> admin`: Full access to Incus.
45-
- `server -> operator`: Full access to Incus, without edit access on server configuration, certificates, or storage pools.
46-
- `server -> viewer`: Can view all server level configuration but cannot edit. Cannot view projects or their contents.
47-
- `project -> manager`: Full access to a single project, including edit access.
48-
- `project -> operator`: Full access to a single project, without edit access.
49-
- `project -> viewer`: View access for a single project.
50-
- `instance -> manager`: Full access to a single instance, including edit access.
51-
- `instance -> operator`: Full access to a single instance, without edit access.
52-
- `instance -> user`: View access to a single instance, plus permissions for `exec`, `console`, and `file` APIs.
53-
- `instance -> viewer`: View access to a single instance.
42+
43+
The full Incus OpenFGA authorization model is defined in `internal/server/auth/driver_openfga_model.openfga`:
44+
45+
```{literalinclude} ../internal/server/auth/driver_openfga_model.openfga
46+
---
47+
language: none
48+
---
49+
```
5450

5551
```{important}
5652
Users that you do not trust with root access to the host should not be granted the following relations:
@@ -68,11 +64,3 @@ Users that you do not trust with root access to the host should not be granted t
6864
The remaining relations may be granted.
6965
However, you must apply appropriate {ref}`project-restrictions`.
7066
```
71-
72-
The full Incus OpenFGA authorization model is defined in `internal/server/auth/driver_openfga_model.openfga`:
73-
74-
```{literalinclude} ../internal/server/auth/driver_openfga_model.openfga
75-
---
76-
language: none
77-
---
78-
```

internal/server/auth/authorization.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type PermissionChecker func(object Object) bool
4141
type Authorizer interface {
4242
Driver() string
4343
StopService(ctx context.Context) error
44+
ApplyPatch(ctx context.Context, name string) error
4445

4546
CheckPermission(ctx context.Context, r *http.Request, object Object, entitlement Entitlement) error
4647
GetPermissionChecker(ctx context.Context, r *http.Request, entitlement Entitlement, objectType ObjectType) (PermissionChecker, error)

internal/server/auth/authorization_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const (
1717
EntitlementCanViewMetrics Entitlement = "can_view_metrics"
1818
EntitlementCanViewPrivilegedEvents Entitlement = "can_view_privileged_events"
1919
EntitlementCanViewResources Entitlement = "can_view_resources"
20+
EntitlementCanViewSensitive Entitlement = "can_view_sensitive"
2021

2122
// Project entitlements.
2223
EntitlementCanCreateImageAliases Entitlement = "can_create_image_aliases"

internal/server/auth/driver_common.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ func (c *commonAuthorizer) StopService(ctx context.Context) error {
134134
return nil
135135
}
136136

137+
// ApplyPatch is a no-op.
138+
func (c *commonAuthorizer) ApplyPatch(ctx context.Context, name string) error {
139+
return nil
140+
}
141+
137142
// AddProject is a no-op.
138143
func (c *commonAuthorizer) AddProject(ctx context.Context, projectID int64, name string) error {
139144
return nil

internal/server/auth/driver_openfga.go

Lines changed: 43 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -144,76 +144,41 @@ func (f *fga) StopService(ctx context.Context) error {
144144
return nil
145145
}
146146

147-
func (f *fga) connect(ctx context.Context, certificateCache *certificate.Cache, opts Opts) error {
148-
var builtinAuthorizationModel client.ClientWriteAuthorizationModelRequest
147+
// ApplyPatch is called when an applicable server patch is run, this triggers a model re-upload.
148+
func (f *fga) ApplyPatch(ctx context.Context, name string) error {
149+
// Upload a new model.
150+
logger.Info("Refreshing the OpenFGA model")
151+
return f.refreshModel(ctx)
152+
}
149153

154+
func (f *fga) refreshModel(ctx context.Context) error {
155+
var builtinAuthorizationModel client.ClientWriteAuthorizationModelRequest
150156
err := json.Unmarshal([]byte(authModel), &builtinAuthorizationModel)
151157
if err != nil {
152-
return err
158+
return fmt.Errorf("Failed to unmarshal built in authorization model: %w", err)
153159
}
154160

155-
// Load current authorization model.
156-
readModelResponse, err := f.client.ReadLatestAuthorizationModel(ctx).Execute()
161+
_, err = f.client.WriteAuthorizationModel(ctx).Body(builtinAuthorizationModel).Execute()
157162
if err != nil {
158-
return fmt.Errorf("Failed to read pre-existing OpenFGA model: %w", err)
163+
return fmt.Errorf("Failed to write the authorization model: %w", err)
159164
}
160165

161-
// Check if we need to upload a new model.
162-
upload := readModelResponse.AuthorizationModel == nil
163-
164-
if !upload {
165-
// Make sure we're not dealing with different schemas.
166-
if readModelResponse.AuthorizationModel.SchemaVersion != builtinAuthorizationModel.SchemaVersion {
167-
return fmt.Errorf("Existing OpenFGA model has schema version %q, but our model has version %q", readModelResponse.AuthorizationModel.SchemaVersion, builtinAuthorizationModel.SchemaVersion)
168-
}
169-
170-
// Clear condition field from older servers.
171-
for _, entry := range readModelResponse.AuthorizationModel.TypeDefinitions {
172-
if entry.Metadata == nil || entry.Metadata.Relations == nil {
173-
continue
174-
}
175-
176-
for _, relation := range *entry.Metadata.Relations {
177-
if relation.DirectlyRelatedUserTypes == nil {
178-
continue
179-
}
180-
181-
for i, reference := range *relation.DirectlyRelatedUserTypes {
182-
if reference.Condition != nil && *reference.Condition == "" {
183-
rel := *relation.DirectlyRelatedUserTypes
184-
rel[i].Condition = nil
185-
}
186-
}
187-
}
188-
}
189-
190-
// Serialize the models to JSON.
191-
existingTypeDefinitions, err := json.Marshal(readModelResponse.AuthorizationModel.TypeDefinitions)
192-
if err != nil {
193-
return fmt.Errorf("Failed to compare OpenFGA model type definitions: %w", err)
194-
}
195-
196-
builtinTypeDefinitions, err := json.Marshal(builtinAuthorizationModel.TypeDefinitions)
197-
if err != nil {
198-
return fmt.Errorf("Failed to compare OpenFGA model type definitions: %w", err)
199-
}
166+
return nil
167+
}
200168

201-
// Compare them.
202-
if string(existingTypeDefinitions) != string(builtinTypeDefinitions) {
203-
logger.Info("The OpenFGA model has changed, uploading new model")
204-
upload = true
205-
}
169+
func (f *fga) connect(ctx context.Context, certificateCache *certificate.Cache, opts Opts) error {
170+
// Load current authorization model.
171+
readModelResponse, err := f.client.ReadLatestAuthorizationModel(ctx).Execute()
172+
if err != nil {
173+
return fmt.Errorf("Failed to read pre-existing OpenFGA model: %w", err)
206174
}
207175

208-
if upload {
209-
err = json.Unmarshal([]byte(authModel), &builtinAuthorizationModel)
210-
if err != nil {
211-
return fmt.Errorf("Failed to unmarshal built in authorization model: %w", err)
212-
}
213-
214-
_, err := f.client.WriteAuthorizationModel(ctx).Body(builtinAuthorizationModel).Execute()
176+
// Check if we need to upload an initial model.
177+
if readModelResponse.AuthorizationModel == nil {
178+
logger.Info("Upload initial OpenFGA model")
179+
err := f.refreshModel(ctx)
215180
if err != nil {
216-
return fmt.Errorf("Failed to write the authorization model: %w", err)
181+
return fmt.Errorf("Failed to load initial model: %w", err)
217182
}
218183
}
219184

@@ -951,27 +916,41 @@ func (f *fga) projectObjects(ctx context.Context, projectName string) ([]string,
951916
return allObjects, nil
952917
}
953918

954-
func (f *fga) syncResources(ctx context.Context, resources Resources) error {
919+
func (f *fga) applyPatches(ctx context.Context) ([]client.ClientTupleKey, []client.ClientTupleKeyWithoutCondition, error) {
955920
var writes []client.ClientTupleKey
956921
var deletions []client.ClientTupleKeyWithoutCondition
957922

958-
// Check if the type-bound public access is set.
923+
// Add the public access permission if not set.
959924
resp, err := f.client.Check(ctx).Body(client.ClientCheckRequest{
960925
User: "user:*",
961-
Relation: "viewer",
926+
Relation: "authenticated",
962927
Object: ObjectServer().String(),
963928
}).Execute()
964929
if err != nil {
965-
return err
930+
return nil, nil, err
966931
}
967932

968-
// If not, set it.
969933
if !resp.GetAllowed() {
970934
writes = append(writes, client.ClientTupleKey{
971935
User: "user:*",
972-
Relation: "viewer",
936+
Relation: "authenticated",
973937
Object: ObjectServer().String(),
974938
})
939+
940+
// Attempt to clear the former version of this permission.
941+
_ = f.updateTuples(ctx, nil, []client.ClientTupleKeyWithoutCondition{
942+
{User: "user:*", Relation: "viewer", Object: ObjectServer().String()},
943+
})
944+
}
945+
946+
return writes, deletions, nil
947+
}
948+
949+
func (f *fga) syncResources(ctx context.Context, resources Resources) error {
950+
// Apply model patches.
951+
writes, deletions, err := f.applyPatches(ctx)
952+
if err != nil {
953+
return err
975954
}
976955

977956
// Helper function for diffing local objects with those in OpenFGA. These are appended to the writes and deletions

internal/server/auth/driver_openfga_model.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/server/auth/driver_openfga_model.openfga

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ type project
7777
define admin: [user, group#member] or admin from server
7878
define operator: [user, group#member] or admin or operator from server
7979
define user: [user, group#member] or operator or user from server
80-
define viewer: [user, group#member] or user
80+
define viewer: [user, group#member] or user or viewer from server
8181
define can_create_image_aliases: [user, group#member] or operator
8282
define can_create_images: [user, group#member] or operator
8383
define can_create_instances: [user, group#member] or operator
@@ -97,17 +97,19 @@ type server
9797
define admin: [user, group#member]
9898
define operator: [user, group#member] or admin
9999
define user: [user, group#member] or operator
100-
define viewer: [user:*] or user
100+
define viewer: [user, group#member] or user
101+
define authenticated: [user:*]
101102
define can_create_certificates: [user, group#member] or admin
102103
define can_create_network_integrations: [user, group#member] or admin
103104
define can_create_projects: [user, group#member] or admin
104105
define can_create_storage_pools: [user, group#member] or admin
105106
define can_edit: admin
106107
define can_override_cluster_target_restriction: [user, group#member] or admin
107-
define can_view_metrics: [user, group#member] or viewer
108108
define can_view_privileged_events: [user, group#member] or admin
109-
define can_view_resources: [user, group#member] or viewer
110-
define can_view: viewer
109+
define can_view_metrics: authenticated
110+
define can_view_resources: authenticated
111+
define can_view_sensitive: [user, group#member] or viewer
112+
define can_view: authenticated
111113

112114
type storage_bucket
113115
relations
@@ -119,7 +121,7 @@ type storage_pool
119121
relations
120122
define server: [server]
121123
define can_edit: [user, group#member] or admin from server
122-
define can_view: viewer from server
124+
define can_view: authenticated from server
123125

124126
type storage_volume
125127
relations

internal/server/auth/driver_tls.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (t *tls) CheckPermission(ctx context.Context, r *http.Request, object Objec
6363
// Check server level object types
6464
switch object.Type() {
6565
case ObjectTypeServer:
66-
if entitlement == EntitlementCanView || entitlement == EntitlementCanViewResources || entitlement == EntitlementCanViewMetrics {
66+
if entitlement == EntitlementCanView || entitlement == EntitlementCanViewResources || entitlement == EntitlementCanViewMetrics || entitlement == EntitlementCanViewSensitive {
6767
return nil
6868
}
6969

@@ -139,7 +139,7 @@ func (t *tls) GetPermissionChecker(ctx context.Context, r *http.Request, entitle
139139
// Check server level object types
140140
switch objectType {
141141
case ObjectTypeServer:
142-
if entitlement == EntitlementCanView || entitlement == EntitlementCanViewResources || entitlement == EntitlementCanViewMetrics {
142+
if entitlement == EntitlementCanView || entitlement == EntitlementCanViewResources || entitlement == EntitlementCanViewMetrics || entitlement == EntitlementCanViewSensitive {
143143
return allowFunc(true), nil
144144
}
145145

test/suites/openfga.sh

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ test_openfga() {
3535
echo "==> Checking permissions for unknown user..."
3636
user_is_not_server_admin
3737
user_is_not_server_operator
38+
user_is_not_server_viewer
3839
user_is_not_project_admin
3940
user_is_not_project_operator
4041

@@ -105,12 +106,17 @@ test_openfga() {
105106
shutdown_openfga
106107
}
107108

108-
user_is_not_server_admin() {
109-
# Can always see server info (type-bound public access https://openfga.dev/docs/modeling/public-access).
110-
incus info oidc-openfga: > /dev/null
109+
user_is_not_server_viewer() {
110+
# Should still be able to list certificates.
111+
[ "$(incus config trust list oidc-openfga: -f csv -cf | wc -l)" = 0 ]
111112

112113
# Cannot see any config.
113114
! incus info oidc-openfga: | grep -Fq 'core.https_address' || false
115+
}
116+
117+
user_is_not_server_admin() {
118+
# Can always see server info (type-bound public access https://openfga.dev/docs/modeling/public-access).
119+
incus info oidc-openfga: > /dev/null
114120

115121
# Cannot set any config.
116122
! incus config set oidc-openfga: core.proxy_https=https://example.com || false
@@ -125,13 +131,6 @@ user_is_not_server_admin() {
125131

126132
# Should not be able to create a storage pool.
127133
! incus storage create oidc-openfga:test dir || false
128-
129-
# Should still be able to list certificates.
130-
[ "$(incus config trust list oidc-openfga: -f csv -cf | wc -l)" = 1 ]
131-
132-
# Cannot edit certificates.
133-
fingerprint="$(incus config trust list -f csv -cf)"
134-
! incus config trust show "${fingerprint}" | sed -e "s/restricted: false/restricted: true/" | incus config trust edit "oidc-openfga:${fingerprint}" || false
135134
}
136135

137136
user_is_not_server_operator() {
@@ -204,7 +203,6 @@ user_is_project_operator() {
204203
}
205204

206205
user_is_not_project_operator() {
207-
208206
# Project list will not fail but there will be no output.
209207
[ "$(incus project list oidc-openfga: -f csv | wc -l)" = 0 ]
210208
! incus project show oidc-openfga:default || false

0 commit comments

Comments
 (0)