diff --git a/internal/dbmodel/sql/postgres/020_add_ssh_keys_table.up.sql b/internal/dbmodel/sql/postgres/020_add_ssh_keys_table.up.sql new file mode 100644 index 000000000..0f78869f0 --- /dev/null +++ b/internal/dbmodel/sql/postgres/020_add_ssh_keys_table.up.sql @@ -0,0 +1,14 @@ +-- Add the ability to store users' SSH public keys + +CREATE TABLE ssh_keys ( + id SERIAL PRIMARY KEY, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE, + public_key BYTEA NOT NULL, + key_comment VARCHAR(255), + identity_name TEXT NOT NULL, + FOREIGN KEY (identity_name) REFERENCES identities(name), + CONSTRAINT unique_identity_ssh_key UNIQUE(identity_name, public_key) +); + +CREATE INDEX idx_ssh_keys_user_id ON ssh_keys (identity_name); diff --git a/internal/dbmodel/sshkeys.go b/internal/dbmodel/sshkeys.go new file mode 100644 index 000000000..8fceac153 --- /dev/null +++ b/internal/dbmodel/sshkeys.go @@ -0,0 +1,23 @@ +// Copyright 2025 Canonical. + +package dbmodel + +import "time" + +// SSHKey holds a user's public SSH key. +type SSHKey struct { + // Note this doesn't use the standard gorm.Model to avoid soft-deletes. + + ID uint `gorm:"primarykey"` + CreatedAt time.Time + UpdatedAt time.Time + + // IdentityName is the unique name (email or client-id) of this entity. + IdentityName string `gorm:"uniqueIndex:unique_identity_ssh_key"` + Identity Identity `gorm:"foreignKey:IdentityName;references:Name"` + + // PublicKey holds the user's public SSH key. + PublicKey []byte `gorm:"uniqueIndex:unique_identity_ssh_key"` + // KeyComment holds a user provided comment. + KeyComment string +} diff --git a/internal/dbmodel/sshkeys_test.go b/internal/dbmodel/sshkeys_test.go new file mode 100644 index 000000000..547954c34 --- /dev/null +++ b/internal/dbmodel/sshkeys_test.go @@ -0,0 +1,35 @@ +// Copyright 2025 Canonical. + +package dbmodel_test + +import ( + "testing" + + qt "github.com/frankban/quicktest" + + "github.com/canonical/jimm/v3/internal/dbmodel" +) + +func TestSSHKeyUniqueConstraint(t *testing.T) { + c := qt.New(t) + db := gormDB(c) + + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(db.Create(u).Error, qt.IsNil) + + key := dbmodel.SSHKey{ + PublicKey: []byte("test"), + Identity: *u, + KeyComment: "foo", + } + c.Assert(db.Create(&key).Error, qt.IsNil) + + newKey := dbmodel.SSHKey{ + PublicKey: []byte("test"), + Identity: *u, + KeyComment: "bar", + } + c.Assert(db.Create(&newKey).Error, qt.ErrorMatches, ".*duplicate key value violates unique constraint \"unique_identity_ssh_key\".*") +}