Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ with no database.
## First run

The root token authenticates as the principal `root` with the `SUPER_ADMIN` authority.
That is enough to administer clusters, SSH keys, templates and deployments right away:
That is enough to administer clusters, SSH credentials, templates and deployments right away:

```bash
TOKEN='<the token printed at startup>'
Expand All @@ -243,7 +243,7 @@ curl -s -X POST localhost:9095/api/v1/slurm-clusters \

### Owning resources requires a matching `users` row

Endpoints that create *owned* resources — SSH keys, cluster configs, data storages,
Endpoints that create *owned* resources — SSH credentials, cluster configs, data storages,
processes — resolve the caller to a user record and refuse if none exists:

```
Expand Down Expand Up @@ -292,17 +292,18 @@ curl -s localhost:9095/api/v1/slurm-clusters # readable without a token
```

Catalogue reads — clusters, partitions, templates and deployments — are open to
anonymous callers. SSH keys are not: a key is private to whoever registered it.
anonymous callers. SSH credentials are not: a credential is private to whoever
registered it.
Beyond those:

- **Writes** require `ADMIN` or `SUPER_ADMIN`, except creating SSH keys, cluster
- **Writes** require `ADMIN` or `SUPER_ADMIN`, except creating SSH credentials, cluster
configs, data storages, data products and processes, which any authenticated caller
may do for themselves.
- **Owner-scoped reads** (`/slurm-cluster-configs/{id}`, `/users/{id}`) require
authentication and are refused unless the caller owns the record, holds a share, or is
an admin. `/ssh-keys/{id}` is stricter still: the owner and nobody else, admins
included, because a key is the credential itself rather than something reached with
one.
an admin. `/ssh-credentials/{id}` is stricter still: the owner and nobody else, admins
included, because a credential *is* the secret material rather than something reached
with it.
- **Unfiltered listings** of cluster configs, data storages, processes and users require
`ADMIN`, because they expose who holds access to what.

Expand Down Expand Up @@ -341,7 +342,7 @@ caller lacking the required role returns `403`.
| `502 Unable to validate bearer token` | CILogon is unreachable. This is reported separately from `401` because it is not the caller's fault. |
| `403 Access denied` on a write | The caller authenticated but lacks `ADMIN`/`SUPER_ADMIN`. Grant the role in `user_roles`. |
| `404 No user record found for authenticated principal` | The caller has no `users` row. Register the user (see "First run" above). The `root` row is created automatically on startup. |
| `409 Key is in use by a credential` | An SSH key cannot be deleted while a credential references it. Delete the credential first. |
| `409 SSH credential <id> is still presented by N record(s)` | A credential cannot be deleted while a cluster config or a data storage still presents it. Repoint or delete those first. |
| `409 Template has deployments` | A template cannot be deleted while deployments reference it. Delete the deployments first. |

---
Expand Down
16 changes: 8 additions & 8 deletions api/compute/dto/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type SlurmClusterConfigRequest struct {
LoginUser string `json:"loginUser"`
WorkRoot string `json:"workRoot"`

SSHKeyID string `json:"sshKeyId"`
SSHCredentialID string `json:"sshCredentialId"`
}

// Validate implements httpx.Validator.
Expand All @@ -50,7 +50,7 @@ func (r *SlurmClusterConfigRequest) Validate() []httpx.FieldError {
c.NotBlank("slurmClusterId", "Slurm cluster id cannot be blank", r.SlurmClusterID)
c.NotBlank("loginUser", "Login user cannot be blank", r.LoginUser)
c.NotBlank("workRoot", "Work root cannot be blank", r.WorkRoot)
c.NotBlank("sshKeyId", "SSH key id cannot be blank", r.SSHKeyID)
c.NotBlank("sshCredentialId", "SSH credential id cannot be blank", r.SSHCredentialID)
return c.Fields()
}

Expand Down Expand Up @@ -81,8 +81,8 @@ type SlurmClusterConfigResponse struct {
LoginUser string `json:"loginUser"`
WorkRoot string `json:"workRoot"`

SSHKeyID *string `json:"sshKeyId"`
SSHKey *creddto.SSHKeyResponse `json:"sshKey"`
SSHCredentialID *string `json:"sshCredentialId"`
SSHCredential *creddto.SSHCredentialResponse `json:"sshCredential"`

Permission *string `json:"permission,omitempty"`
}
Expand All @@ -96,15 +96,15 @@ func ToSlurmClusterConfigResponse(c *model.SlurmClusterConfig) SlurmClusterConfi
SlurmClusterID: c.SlurmClusterID,
LoginUser: c.LoginUser,
WorkRoot: c.WorkRoot,
SSHKeyID: c.SSHKeyID,
SSHCredentialID: c.SSHCredentialID,
}
if c.SlurmCluster != nil {
cluster := ToSlurmClusterResponse(c.SlurmCluster)
out.SlurmCluster = &cluster
}
if c.SSHKey != nil {
key := creddto.ToSSHKeyResponse(c.SSHKey)
out.SSHKey = &key
if c.SSHCredential != nil {
key := creddto.ToSSHCredentialResponse(c.SSHCredential)
out.SSHCredential = &key
}
return out
}
Expand Down
4 changes: 2 additions & 2 deletions api/compute/model/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ type SlurmClusterConfig struct {

WorkRoot string `gorm:"column:work_root;type:varchar(1024);not null" json:"workRoot"`

SSHKeyID *string `gorm:"column:ssh_key_id;type:varchar(36);index" json:"sshKeyId,omitempty"`
SSHKey *cred.SSHKey `gorm:"references:ID;constraint:OnDelete:RESTRICT,OnUpdate:CASCADE" json:"sshKey,omitempty"`
SSHCredentialID *string `gorm:"column:ssh_credential_id;type:varchar(36);index" json:"sshCredentialId,omitempty"`
SSHCredential *cred.SSHCredential `gorm:"references:ID;constraint:OnDelete:RESTRICT,OnUpdate:CASCADE" json:"sshCredential,omitempty"`

// OwnerID is named for its role because ownership, not mere reference, is what the
// authorisation checks read. RESTRICT: a user who still owns configs cannot be
Expand Down
6 changes: 3 additions & 3 deletions api/compute/repository/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (r *SlurmClusterConfigRepository) withReferences(ctx context.Context) *gorm
return r.db.WithContext(ctx).
Preload("SlurmCluster").
Preload("SlurmCluster.Partitions").
Preload("SSHKey")
Preload("SSHCredential")
}

// FindAll returns every config.
Expand Down Expand Up @@ -102,14 +102,14 @@ func (r *SlurmClusterConfigRepository) FindByIDsExcludingOwner(ctx context.Conte
return out, err
}

// CountByKeyID reports how many configs still present one SSH key. The credentials
// CountByKeyID reports how many configs still present one SSH credential. The credentials
// vertical asks before deleting a key: the foreign key is RESTRICT, so without this the
// delete would fail as an opaque constraint violation rather than a 409 naming what
// still holds it.
func (r *SlurmClusterConfigRepository) CountByKeyID(ctx context.Context, keyID string) (int64, error) {
var n int64
err := r.db.WithContext(ctx).Model(&model.SlurmClusterConfig{}).
Where("ssh_key_id = ?", keyID).Count(&n).Error
Where("ssh_credential_id = ?", keyID).Count(&n).Error
return n, err
}

Expand Down
28 changes: 14 additions & 14 deletions api/compute/service/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func (a *ConfigAccess) RequireUsable(ctx context.Context, id string) (*model.Slu
// SlurmClusterConfigService manages the login configs jobs are submitted through.
//
// Registering one is self-service: any authenticated caller may declare how they reach
// a cluster from the catalogue, under one of their own SSH keys, and it belongs to
// a cluster from the catalogue, under one of their own SSH credentials, and it belongs to
// them. Everyone else reaches it through its sharing rules — which is how they submit
// under a key they do not hold.
type SlurmClusterConfigService struct {
Expand Down Expand Up @@ -257,26 +257,26 @@ func (s *SlurmClusterConfigService) Get(ctx context.Context, id string) (*dto.Sl
// the caller to own it; keeping the one already there does not, so a grantee with WRITE
// can edit a config without owning the key it logs in with — and still cannot point it
// at a key of their own.
func (s *SlurmClusterConfigService) resolveReferences(ctx context.Context, tx *gorm.DB, req *dto.SlurmClusterConfigRequest, held *string) (*model.SlurmCluster, *credmodel.SSHKey, error) {
func (s *SlurmClusterConfigService) resolveReferences(ctx context.Context, tx *gorm.DB, req *dto.SlurmClusterConfigRequest, held *string) (*model.SlurmCluster, *credmodel.SSHCredential, error) {
cluster, err := s.clusters.WithTx(tx).FindByID(ctx, req.SlurmClusterID)
if err != nil {
return nil, nil, notFoundAs(err, "Slurm cluster not found: %s", req.SlurmClusterID)
}

keys := s.keys.WithTx(tx)
if held != nil && *held == req.SSHKeyID {
key, err := keys.Find(ctx, req.SSHKeyID)
if held != nil && *held == req.SSHCredentialID {
key, err := keys.Find(ctx, req.SSHCredentialID)
return cluster, key, err
}
key, err := keys.RequireOwned(ctx, req.SSHKeyID)
key, err := keys.RequireOwned(ctx, req.SSHCredentialID)
if err != nil {
return nil, nil, err
}
return cluster, key, nil
}

// Create registers a config owned by the calling user, against an existing cluster and
// an existing SSH key.
// an existing SSH credential.
//
// The owner is taken from the token, so there is no way to register a config on
// someone else's behalf.
Expand All @@ -300,11 +300,11 @@ func (s *SlurmClusterConfigService) Create(ctx context.Context, req *dto.SlurmCl
}

config := &model.SlurmClusterConfig{
SlurmClusterID: cluster.ID,
SlurmCluster: cluster,
SSHKeyID: &key.ID,
SSHKey: key,
OwnerID: &owner.ID,
SlurmClusterID: cluster.ID,
SlurmCluster: cluster,
SSHCredentialID: &key.ID,
SSHCredential: key,
OwnerID: &owner.ID,
}
dto.ApplySlurmClusterConfigRequest(config, req)
if err := configs.Save(ctx, config); err != nil {
Expand Down Expand Up @@ -337,16 +337,16 @@ func (s *SlurmClusterConfigService) Update(ctx context.Context, id string, req *
if err != nil {
return err
}
cluster, key, err := s.resolveReferences(ctx, tx, req, config.SSHKeyID)
cluster, key, err := s.resolveReferences(ctx, tx, req, config.SSHCredentialID)
if err != nil {
return err
}

dto.ApplySlurmClusterConfigRequest(config, req)
config.SlurmClusterID = cluster.ID
config.SlurmCluster = cluster
config.SSHKeyID = &key.ID
config.SSHKey = key
config.SSHCredentialID = &key.ID
config.SSHCredential = key
if err := configs.Save(ctx, config); err != nil {
return err
}
Expand Down
20 changes: 10 additions & 10 deletions api/compute/service/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ func newConfigFixture(t *testing.T) *configFixture {
if err := gdb.Create(cluster).Error; err != nil {
t.Fatalf("create cluster: %v", err)
}
key := &credmodel.SSHKey{SSHKeyName: "alice-key", PublicKey: "ssh-ed25519 AAAA", PrivateKey: "secret", OwnerID: "alice"}
key := &credmodel.SSHCredential{SSHCredentialName: "alice-key", PublicKey: "ssh-ed25519 AAAA", PrivateKey: "secret", OwnerID: "alice"}
if err := gdb.Create(key).Error; err != nil {
t.Fatalf("create key: %v", err)
}

configs := computerepo.NewSlurmClusterConfigRepository(gdb)
shares := sharingrepo.NewRepository(gdb)
clusters := computerepo.NewSlurmClusterRepository(gdb)
keys := credrepo.NewSSHKeyRepository(gdb)
keys := credrepo.NewSSHCredentialRepository(gdb)
users := iamrepo.NewUserRepository(gdb)
groups := iamrepo.NewGroupRepository(gdb)
members := iamrepo.NewGroupMemberRepository(gdb)
Expand All @@ -114,11 +114,11 @@ func newConfigFixture(t *testing.T) *configFixture {

func (f *configFixture) req() *dto.SlurmClusterConfigRequest {
return &dto.SlurmClusterConfigRequest{
Name: str("expanse allocation"),
SlurmClusterID: f.clusterID,
LoginUser: "alice",
WorkRoot: "/scratch/alice",
SSHKeyID: f.keyID,
Name: str("expanse allocation"),
SlurmClusterID: f.clusterID,
LoginUser: "alice",
WorkRoot: "/scratch/alice",
SSHCredentialID: f.keyID,
}
}

Expand Down Expand Up @@ -148,8 +148,8 @@ func TestConfigCreateTakesOwnerFromToken(t *testing.T) {
if out.SlurmCluster == nil || out.SlurmCluster.ClusterName != "expanse" {
t.Errorf("slurmCluster = %+v, want the expanse cluster inlined", out.SlurmCluster)
}
if out.SSHKey == nil || out.SSHKey.SSHKeyName != "alice-key" {
t.Errorf("sshKey = %+v, want the key summary inlined", out.SSHKey)
if out.SSHCredential == nil || out.SSHCredential.SSHCredentialName != "alice-key" {
t.Errorf("sshCredential = %+v, want the key summary inlined", out.SSHCredential)
}
}

Expand Down Expand Up @@ -350,7 +350,7 @@ func TestConfigReferencesMustResolve(t *testing.T) {
}

req = f.req()
req.SSHKeyID = "no-such-key"
req.SSHCredentialID = "no-such-key"
if _, err := f.configs.Create(f.owner, req); httpx.StatusOf(err) != 404 {
t.Errorf("unknown key = %v (status %d), want 404", err, httpx.StatusOf(err))
}
Expand Down
46 changes: 23 additions & 23 deletions api/credentials/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* under the License.
*/

// Package controller serves the SSH key routes.
// Package controller serves the SSH credential routes.
package controller

import (
Expand All @@ -30,24 +30,24 @@ import (
"github.com/apache/airavata/api/credentials/service"
)

// SSHKeyController serves /api/v1/ssh-keys.
type SSHKeyController struct{ svc *service.SSHKeyService }
// SSHCredentialController serves /api/v1/ssh-credentials.
type SSHCredentialController struct{ svc *service.SSHCredentialService }

// NewSSHKeyController returns a handler delegating to svc.
func NewSSHKeyController(svc *service.SSHKeyService) *SSHKeyController {
return &SSHKeyController{svc: svc}
// NewSSHCredentialController returns a handler delegating to svc.
func NewSSHCredentialController(svc *service.SSHCredentialService) *SSHCredentialController {
return &SSHCredentialController{svc: svc}
}

// Register mounts the SSH key routes.
func (h *SSHKeyController) Register(mux *http.ServeMux) {
mux.HandleFunc("GET /api/v1/ssh-keys", h.list)
mux.HandleFunc("POST /api/v1/ssh-keys", h.create)
mux.HandleFunc("GET /api/v1/ssh-keys/{sshKeyId}", h.get)
mux.HandleFunc("PUT /api/v1/ssh-keys/{sshKeyId}", h.update)
mux.HandleFunc("DELETE /api/v1/ssh-keys/{sshKeyId}", h.delete)
// Register mounts the SSH credential routes.
func (h *SSHCredentialController) Register(mux *http.ServeMux) {
mux.HandleFunc("GET /api/v1/ssh-credentials", h.list)
mux.HandleFunc("POST /api/v1/ssh-credentials", h.create)
mux.HandleFunc("GET /api/v1/ssh-credentials/{sshCredentialId}", h.get)
mux.HandleFunc("PUT /api/v1/ssh-credentials/{sshCredentialId}", h.update)
mux.HandleFunc("DELETE /api/v1/ssh-credentials/{sshCredentialId}", h.delete)
}

func (h *SSHKeyController) list(w http.ResponseWriter, r *http.Request) {
func (h *SSHCredentialController) list(w http.ResponseWriter, r *http.Request) {
keys, err := h.svc.List(r.Context())
if err != nil {
httpx.WriteError(w, r, err)
Expand All @@ -56,17 +56,17 @@ func (h *SSHKeyController) list(w http.ResponseWriter, r *http.Request) {
httpx.WriteJSON(w, http.StatusOK, keys)
}

func (h *SSHKeyController) get(w http.ResponseWriter, r *http.Request) {
key, err := h.svc.Get(r.Context(), r.PathValue("sshKeyId"))
func (h *SSHCredentialController) get(w http.ResponseWriter, r *http.Request) {
key, err := h.svc.Get(r.Context(), r.PathValue("sshCredentialId"))
if err != nil {
httpx.WriteError(w, r, err)
return
}
httpx.WriteJSON(w, http.StatusOK, key)
}

func (h *SSHKeyController) create(w http.ResponseWriter, r *http.Request) {
var req dto.SSHKeyRequest
func (h *SSHCredentialController) create(w http.ResponseWriter, r *http.Request) {
var req dto.SSHCredentialRequest
if err := httpx.Bind(r, &req); err != nil {
httpx.WriteError(w, r, err)
return
Expand All @@ -79,22 +79,22 @@ func (h *SSHKeyController) create(w http.ResponseWriter, r *http.Request) {
httpx.WriteJSON(w, http.StatusCreated, key)
}

func (h *SSHKeyController) update(w http.ResponseWriter, r *http.Request) {
var req dto.SSHKeyRequest
func (h *SSHCredentialController) update(w http.ResponseWriter, r *http.Request) {
var req dto.SSHCredentialRequest
if err := httpx.Bind(r, &req); err != nil {
httpx.WriteError(w, r, err)
return
}
key, err := h.svc.Update(r.Context(), r.PathValue("sshKeyId"), &req)
key, err := h.svc.Update(r.Context(), r.PathValue("sshCredentialId"), &req)
if err != nil {
httpx.WriteError(w, r, err)
return
}
httpx.WriteJSON(w, http.StatusOK, key)
}

func (h *SSHKeyController) delete(w http.ResponseWriter, r *http.Request) {
if err := h.svc.Delete(r.Context(), r.PathValue("sshKeyId")); err != nil {
func (h *SSHCredentialController) delete(w http.ResponseWriter, r *http.Request) {
if err := h.svc.Delete(r.Context(), r.PathValue("sshCredentialId")); err != nil {
httpx.WriteError(w, r, err)
return
}
Expand Down
Loading
Loading