Skip to content

Commit

Permalink
Merge pull request #1939 from chirino/multi-org-owners
Browse files Browse the repository at this point in the history
apiserver: support multiple Organization owners
  • Loading branch information
mergify[bot] authored Feb 23, 2024
2 parents d8f71df + 6d70054 commit e108372
Show file tree
Hide file tree
Showing 28 changed files with 492 additions and 81 deletions.
14 changes: 14 additions & 0 deletions cmd/nexctl/invitation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"github.com/nexodus-io/nexodus/internal/api/public"
"github.com/urfave/cli/v3"
"strings"
)

func createInvitationCommand() *cli.Command {
Expand Down Expand Up @@ -35,6 +36,12 @@ func createInvitationCommand() *cli.Command {
Name: "organization-id",
Required: false,
},
&cli.StringSliceFlag{
Name: "role",
Required: false,
DefaultText: "member",
Value: []string{"member"},
},
},
Action: func(ctx context.Context, command *cli.Command) error {
organizationId, err := getUUID(command, "organization-id")
Expand All @@ -45,10 +52,13 @@ func createInvitationCommand() *cli.Command {
if err != nil {
return err
}
role := command.StringSlice("role")

return createInvitation(ctx, command, public.ModelsAddInvitation{
OrganizationId: organizationId,
UserId: userId,
Email: command.String("email"),
Roles: role,
})
},
},
Expand Down Expand Up @@ -102,6 +112,10 @@ func invitationsTableFields() []TableField {
return fmt.Sprintf("%s <%s>", inv.From.FullName, inv.From.Username)
}})
fields = append(fields, TableField{Header: "EMAIL", Field: "Email"})
fields = append(fields, TableField{Header: "ROLES", Formatter: func(item interface{}) string {
inv := item.(public.ModelsInvitation)
return strings.Join(inv.Roles, ", ")
}})
fields = append(fields, TableField{Header: "EXPIRES AT", Field: "ExpiresAt"})
return fields
}
Expand Down
10 changes: 6 additions & 4 deletions integration-tests/features/invitation-api.feature
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Feature: Invitations API
"expires_at": "${response.expires_at}",
"id": "${invitation_id}",
"organization_id": "${thompson_user_id}",
"roles": ["member"],
"email": "${johnson_user_id}@redhat.com",
"user_id": "${johnson_user_id}"
}
Expand Down Expand Up @@ -102,6 +103,7 @@ Feature: Invitations API
"expires_at": "${response.expires_at}",
"id": "${invitation_id}",
"organization_id": "${thompson_organization_id}",
"roles": ["member"],
"user_id": "${johnson_user_id}"
}
"""
Expand All @@ -123,11 +125,11 @@ Feature: Invitations API
"picture": "",
"username": "${thompson_username}"
},
"roles": ["member"],
"organization": {
"description": "${thompson_username}'s organization",
"id": "${thompson_organization_id}",
"name": "${thompson_username}",
"owner_id": "${thompson_user_id}"
"name": "${thompson_username}"
},
"user_id": "${johnson_user_id}"
}
Expand All @@ -150,11 +152,11 @@ Feature: Invitations API
"picture": "",
"username": "${thompson_username}"
},
"roles": ["member"],
"organization": {
"description": "${thompson_username}'s organization",
"id": "${thompson_organization_id}",
"name": "${thompson_username}",
"owner_id": "${thompson_user_id}"
"name": "${thompson_username}"
},
"user_id": "${johnson_user_id}"
}
Expand Down
3 changes: 1 addition & 2 deletions integration-tests/features/organization-api.feature
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ Feature: Organization API
{
"description": "${oscar_username}'s organization",
"id": "${oscar_organization_id}",
"name": "${oscar_username}",
"owner_id": "${oscar_user_id}"
"name": "${oscar_username}"
}
]
"""
Expand Down
8 changes: 8 additions & 0 deletions integration-tests/features/user-organization-api.feature
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Feature: Invitations API
[{
"organization_id": "${brent_organization.id}",
"user_id": "${brent_user.id}",
"roles": ["owner"],
"user": ${brent_user}
}]
"""
Expand Down Expand Up @@ -106,14 +107,17 @@ Feature: Invitations API
[{
"organization_id": "${brent_organization.id}",
"user_id": "${anil_user.id}",
"roles": ["member"],
"user": ${anil_user}
}, {
"organization_id": "${brent_organization.id}",
"user_id": "${brent_user.id}",
"roles": ["owner"],
"user": ${brent_user}
}, {
"organization_id": "${brent_organization.id}",
"user_id": "${russel_user.id}",
"roles": ["member"],
"user": ${russel_user}
}]
"""
Expand Down Expand Up @@ -150,6 +154,7 @@ Feature: Invitations API
{
"organization_id": "${brent_organization.id}",
"user_id": "${anil_user.id}",
"roles": ["member"],
"user": ${anil_user}
}
"""
Expand All @@ -162,6 +167,7 @@ Feature: Invitations API
{
"organization_id": "${brent_organization.id}",
"user_id": "${anil_user.id}",
"roles": ["member"],
"user": ${anil_user}
}
"""
Expand All @@ -173,10 +179,12 @@ Feature: Invitations API
[{
"organization_id": "${brent_organization.id}",
"user_id": "${brent_user.id}",
"roles": ["owner"],
"user": ${brent_user}
}, {
"organization_id": "${brent_organization.id}",
"user_id": "${russel_user.id}",
"roles": ["member"],
"user": ${russel_user}
}]
"""
5 changes: 3 additions & 2 deletions internal/api/public/model_models_add_invitation.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/api/public/model_models_invitation.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion internal/api/public/model_models_organization.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/api/public/model_models_user_organization.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
_ "github.com/nexodus-io/nexodus/internal/database/migration_20231130_0000"
_ "github.com/nexodus-io/nexodus/internal/database/migration_20231206_0000"
_ "github.com/nexodus-io/nexodus/internal/database/migration_20231211_0000"
_ "github.com/nexodus-io/nexodus/internal/database/migration_20240221_0000"
"sort"

"github.com/cenkalti/backoff/v4"
Expand Down
81 changes: 81 additions & 0 deletions internal/database/datatype/string_array.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package datatype

import (
"context"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"github.com/lib/pq"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
)

type StringArray []string

// GormDataType gorm common data type
func (StringArray) GormDataType() string {
return "string_array"
}

// GormDBDataType gorm db data type
func (StringArray) GormDBDataType(db *gorm.DB, field *schema.Field) string {
switch db.Dialector.Name() {
case "postgres":
return "text[]"
default:
return "JSON"
}
}

// Value return json value, implement driver.Valuer interface
func (j StringArray) Value() (driver.Value, error) {
return json.Marshal(j)
}

func (j StringArray) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
switch db.Dialector.Name() {
case "postgres":
return gorm.Expr("?", pq.StringArray(j))
default:
data, err := json.Marshal(j)
if err != nil {
db.Error = err
}
return gorm.Expr("?", string(data))
}
}

// implements sql.Scanner interface
func (j *StringArray) Scan(value interface{}) error {
var bytes []byte
switch v := value.(type) {
case []byte:
bytes = v
case string:
bytes = []byte(v)
default:
return errors.New(fmt.Sprint("Failed to unmarshal string array value:", value))
}

if len(bytes) == 0 {
*j = nil
return nil
}
if bytes[0] == '[' {
err := json.Unmarshal(bytes, j)
return err
}
if bytes[0] == '{' {
// Unmarshal as Postgres text array
var a pq.StringArray
err := a.Scan(value)
if err != nil {
return err
}
*j = StringArray(a)
return nil
}
return errors.New(fmt.Sprint("Failed to unmarshal string array value:", value))
}
111 changes: 111 additions & 0 deletions internal/database/migration_20240221_0000/migration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package migration_20240221_0000

import (
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/nexodus-io/nexodus/internal/database/datatype"
. "github.com/nexodus-io/nexodus/internal/database/migrations"
"github.com/nexodus-io/nexodus/internal/models"
"gorm.io/gorm"
)

type UserOrganization struct {
Roles datatype.StringArray `json:"roles" swaggertype:"array,string"`
}
type Invitation struct {
Roles datatype.StringArray `json:"roles" swaggertype:"array,string"`
}

func init() {
migrationId := "20240221-0000"
CreateMigrationFromActions(migrationId,
AddTableColumnsAction(&UserOrganization{}),
AddTableColumnsAction(&Invitation{}),
ExecActionIf(
`CREATE INDEX IF NOT EXISTS "idx_user_organizations_roles" ON "user_organizations" USING GIN ("roles")`,
`DROP INDEX IF EXISTS idx_user_organizations_roles`,
NotOnSqlLite,
),

// While inspecting the DB I realized a bunch of id columns are not uuids... this should fix that
ChangeColumnTypeActionIf(`devices`, `owner_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`devices`, `vpc_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`devices`, `organization_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`devices`, `security_group_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`devices`, `reg_key_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`organizations`, `owner_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`reg_keys`, `owner_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`reg_keys`, `vpc_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`reg_keys`, `organization_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`reg_keys`, `device_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`reg_keys`, `security_group_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`security_groups`, `organization_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`security_groups`, `vpc_id`, `text`, `uuid`, NotOnSqlLite),
ChangeColumnTypeActionIf(`vpcs`, `organization_id`, `text`, `uuid`, NotOnSqlLite),

func(tx *gorm.DB, apply bool) error {
if apply && NotOnSqlLite(tx) {

// this will fill in the role for the user_organizations table
type UserOrganization struct {
UserID uuid.UUID `json:"user_id" gorm:"type:uuid;primary_key"`
OrganizationID uuid.UUID `json:"organization_id" gorm:"type:uuid;primary_key"`
Roles pq.StringArray `json:"roles" gorm:"type:text[]" swaggertype:"array,string"`
}

result := tx.Model(&models.UserOrganization{}).
Where("roles IS NULL").
Update("roles", pq.StringArray{"member"})
if result.Error != nil {
return result.Error
}

type Organization struct {
ID uuid.UUID
OwnerID uuid.UUID
}
rows := []Organization{}

// make all sure all orgs have a member with the owner role
sql := `SELECT DISTINCT id, owner_id FROM organizations LEFT JOIN user_organizations ON user_organizations.organization_id = organizations.id WHERE organization_id is NULL`
result = tx.Raw(sql).FindInBatches(&rows, 100, func(tx *gorm.DB, batch int) error {
for _, r := range rows {
result := tx.Create(&UserOrganization{
UserID: r.OwnerID,
OrganizationID: r.ID,
Roles: []string{"owner"},
})
if result.Error != nil {
return result.Error
}
}
return nil
})
if result.Error != nil {
return result.Error
}

// make sure the owner's memberhip has the owner role
sql = `select * FROM organizations LEFT JOIN user_organizations ON user_organizations.organization_id = organizations.id WHERE organizations.owner_id = user_organizations.user_id AND roles <> '{owner}'`
result = tx.Raw(sql).FindInBatches(&rows, 100, func(tx *gorm.DB, batch int) error {
for _, r := range rows {
result := tx.Save(&UserOrganization{
UserID: r.OwnerID,
OrganizationID: r.ID,
Roles: []string{"owner"},
})
if result.Error != nil {
return result.Error
}
}
return nil
})
if result.Error != nil {
return result.Error
}

}
return nil
},
)
}
Loading

0 comments on commit e108372

Please sign in to comment.