refactor: no access list for Databricks scopes: it was the one request of its own
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
17054ad0ec
commit
1d8a7d2842
6 files changed
+4
-76
No files matched your search
@@ -17,10 +17,6 @@ When Databricks refuses, the pane says what to check: access to the
|
|||||||
workspace and READ on the scope, or, when the workspace's IP access list
|
workspace and READ on the scope, or, when the workspace's IP access list
|
||||||
refused, the network you're on.
|
refused, the network you're on.
|
||||||
|
|
||||||
On a scope, the right pane shows who has access to it and how much
|
|
||||||
(manage, write, read). Databricks shows that only to the scope's
|
|
||||||
managers; to others it says so.
|
|
||||||
|
|
||||||
Hide profiles you don't want in the [config](config.md):
|
Hide profiles you don't want in the [config](config.md):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -140,15 +140,8 @@ func workspaceAPI(t *testing.T, status int) *httptest.Server {
|
|||||||
map[string]any{"key": "storage-key", "last_updated_timestamp": 1767225600000},
|
map[string]any{"key": "storage-key", "last_updated_timestamp": 1767225600000},
|
||||||
map[string]any{"key": "client-secret"},
|
map[string]any{"key": "client-secret"},
|
||||||
}})
|
}})
|
||||||
case r.URL.Path == "/api/2.0/secrets/acls/list" && q.Get("scope") == "app":
|
|
||||||
write(w, 200, map[string]any{"items": []any{
|
|
||||||
map[string]any{"principal": "users", "permission": "READ"},
|
|
||||||
map[string]any{"principal": "admins", "permission": "MANAGE"},
|
|
||||||
}})
|
|
||||||
case r.URL.Path == "/api/2.0/secrets/list" && q.Get("scope") == "locked":
|
case r.URL.Path == "/api/2.0/secrets/list" && q.Get("scope") == "locked":
|
||||||
write(w, 403, map[string]any{"error_code": "PERMISSION_DENIED", "message": "User does not have READ permission on scope locked"})
|
write(w, 403, map[string]any{"error_code": "PERMISSION_DENIED", "message": "User does not have READ permission on scope locked"})
|
||||||
case r.URL.Path == "/api/2.0/secrets/acls/list" && q.Get("scope") == "locked":
|
|
||||||
write(w, 403, map[string]any{"error_code": "PERMISSION_DENIED", "message": "User does not have MANAGE permission"})
|
|
||||||
case r.URL.Path == "/api/2.0/secrets/get" && q.Get("scope") == "app":
|
case r.URL.Path == "/api/2.0/secrets/get" && q.Get("scope") == "app":
|
||||||
write(w, 200, map[string]any{"key": q.Get("key"),
|
write(w, 200, map[string]any{"key": q.Get("key"),
|
||||||
"value": base64.StdEncoding.EncodeToString([]byte("value-of-" + q.Get("key")))})
|
"value": base64.StdEncoding.EncodeToString([]byte("value-of-" + q.Get("key")))})
|
||||||
@@ -301,23 +294,6 @@ func TestClassify(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestScopeDetails(t *testing.T) {
|
|
||||||
p := newTestProvider(t, workspaceAPI(t, 0), nil)
|
|
||||||
src := provider.Source{ID: "prod"}
|
|
||||||
attrs, err := p.Details(context.Background(), provider.Container{Source: src, Name: "app"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
want := []provider.Attr{{Label: "access", Value: "admins · manage"}, {Value: "users · read"}}
|
|
||||||
if !reflect.DeepEqual(attrs, want) {
|
|
||||||
t.Errorf("app: %+v", attrs)
|
|
||||||
}
|
|
||||||
attrs, err = p.Details(context.Background(), provider.Container{Source: src, Name: "locked"})
|
|
||||||
if err != nil || len(attrs) != 1 || !strings.Contains(attrs[0].Value, "managers") {
|
|
||||||
t.Errorf("locked: %+v, %v", attrs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeniedSaysWhatToCheck(t *testing.T) {
|
func TestDeniedSaysWhatToCheck(t *testing.T) {
|
||||||
p := newTestProvider(t, workspaceAPI(t, 0), nil)
|
p := newTestProvider(t, workspaceAPI(t, 0), nil)
|
||||||
_, err := p.Objects(context.Background(), provider.Container{Source: provider.Source{ID: "prod"}, Name: "locked"}, provider.TypeSecret)
|
_, err := p.Objects(context.Background(), provider.Container{Source: provider.Source{ID: "prod"}, Name: "locked"}, provider.TypeSecret)
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package databricks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/databricks/databricks-sdk-go/apierr"
|
|
||||||
"github.com/databricks/databricks-sdk-go/service/workspace"
|
|
||||||
|
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Details are who has access to a scope, and how much. Only the scope's
|
|
||||||
// managers may see that; to others it says so.
|
|
||||||
func (p *Provider) Details(ctx context.Context, c provider.Container) ([]provider.Attr, error) {
|
|
||||||
w, pr, err := p.client(c.Source.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
acls, err := w.ListAclsAll(ctx, workspace.ListAclsRequest{Scope: c.Name})
|
|
||||||
var api *apierr.APIError
|
|
||||||
switch {
|
|
||||||
case errors.As(err, &api) && api.StatusCode == 403:
|
|
||||||
return []provider.Attr{{Label: "access", Value: "only the scope's managers can see who has it"}}, nil
|
|
||||||
case err != nil:
|
|
||||||
return nil, classify(pr, err)
|
|
||||||
case len(acls) == 0:
|
|
||||||
return []provider.Attr{{Label: "access", Value: "workspace admins only"}}, nil
|
|
||||||
}
|
|
||||||
rank := map[workspace.AclPermission]int{workspace.AclPermissionManage: 0, workspace.AclPermissionWrite: 1, workspace.AclPermissionRead: 2}
|
|
||||||
slices.SortFunc(acls, func(a, b workspace.AclItem) int {
|
|
||||||
return cmp.Or(cmp.Compare(rank[a.Permission], rank[b.Permission]), cmp.Compare(a.Principal, b.Principal))
|
|
||||||
})
|
|
||||||
out := make([]provider.Attr, len(acls))
|
|
||||||
for i, a := range acls {
|
|
||||||
out[i] = provider.Attr{Value: a.Principal + " · " + strings.ToLower(string(a.Permission))}
|
|
||||||
}
|
|
||||||
out[0].Label = "access"
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
@@ -221,9 +221,8 @@ func (d *Denied) Error() string {
|
|||||||
func (d *Denied) Unwrap() error { return d.Err }
|
func (d *Denied) Unwrap() error { return d.Err }
|
||||||
|
|
||||||
// ContainerDetails is implemented by providers that can say more about a
|
// ContainerDetails is implemented by providers that can say more about a
|
||||||
// container than its listing does: a vault's settings, who has access to
|
// container than its listing does: how a file is encrypted and what git
|
||||||
// a scope, how a file is encrypted and what git knows of it. It may go
|
// knows of it. ErrUnsupported means there's nothing more for c.
|
||||||
// over the network; ErrUnsupported means there's nothing more for c.
|
|
||||||
type ContainerDetails interface {
|
type ContainerDetails interface {
|
||||||
Details(ctx context.Context, c Container) ([]Attr, error)
|
Details(ctx context.Context, c Container) ([]Attr, error)
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ const (
|
|||||||
slotDescribe // extra metadata for the selected object
|
slotDescribe // extra metadata for the selected object
|
||||||
slotCopy // fetch a value straight to the clipboard
|
slotCopy // fetch a value straight to the clipboard
|
||||||
slotVersions // count the selected object's versions
|
slotVersions // count the selected object's versions
|
||||||
slotDetails // a vault's, scope's or file's details
|
slotDetails // a file's details
|
||||||
slotCount
|
slotCount
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
// The fourth pane shows Fields for anything but a secret. On a provider's
|
// The fourth pane shows Fields for anything but a secret. On a provider's
|
||||||
// row: its account, from what the provider knows locally.
|
// row: its account, from what the provider knows locally.
|
||||||
|
|
||||||
// A vault's, scope's or file's details beyond its listing load once a
|
// A file's details beyond its listing load once a
|
||||||
// session, when the cursor rests on it, and again with refresh. They're
|
// session, when the cursor rests on it, and again with refresh. They're
|
||||||
// extra: moving on cancels the load, and search never waits for them.
|
// extra: moving on cancels the load, and search never waits for them.
|
||||||
type detailsState struct {
|
type detailsState struct {
|
||||||
|
|||||||
Reference in new issue
Block a user