Compare commits
3 Commits
fix/teams-
...
fix/teams-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
832e4b27a8 | ||
|
|
0a69910e8b | ||
|
|
576791d19e |
@@ -11,7 +11,7 @@ from typing import Optional
|
|||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .models import User, Session as UserSession, APIKey, Team, TeamMembership
|
from .models import User, Session as UserSession, APIKey
|
||||||
from .config import get_settings
|
from .config import get_settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -363,8 +363,6 @@ def create_default_admin(db: Session) -> Optional[User]:
|
|||||||
|
|
||||||
The admin password can be set via ORCHARD_ADMIN_PASSWORD environment variable.
|
The admin password can be set via ORCHARD_ADMIN_PASSWORD environment variable.
|
||||||
If not set, defaults to 'changeme123' and requires password change on first login.
|
If not set, defaults to 'changeme123' and requires password change on first login.
|
||||||
|
|
||||||
Also creates the "Global Admins" team and adds the admin user to it.
|
|
||||||
"""
|
"""
|
||||||
# Check if any users exist
|
# Check if any users exist
|
||||||
user_count = db.query(User).count()
|
user_count = db.query(User).count()
|
||||||
@@ -387,27 +385,6 @@ def create_default_admin(db: Session) -> Optional[User]:
|
|||||||
must_change_password=must_change,
|
must_change_password=must_change,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create Global Admins team and add admin to it
|
|
||||||
global_admins_team = Team(
|
|
||||||
name="Global Admins",
|
|
||||||
slug="global-admins",
|
|
||||||
description="System administrators with full access",
|
|
||||||
created_by="admin",
|
|
||||||
)
|
|
||||||
db.add(global_admins_team)
|
|
||||||
db.flush()
|
|
||||||
|
|
||||||
membership = TeamMembership(
|
|
||||||
team_id=global_admins_team.id,
|
|
||||||
user_id=admin.id,
|
|
||||||
role="owner",
|
|
||||||
invited_by="admin",
|
|
||||||
)
|
|
||||||
db.add(membership)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
logger.info("Created Global Admins team and added admin as owner")
|
|
||||||
|
|
||||||
if settings.admin_password:
|
if settings.admin_password:
|
||||||
logger.info("Created default admin user with configured password")
|
logger.info("Created default admin user with configured password")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -287,33 +287,29 @@ def _run_migrations():
|
|||||||
""",
|
""",
|
||||||
# Teams and multi-tenancy migration (009_teams.sql)
|
# Teams and multi-tenancy migration (009_teams.sql)
|
||||||
"""
|
"""
|
||||||
-- Create teams table
|
|
||||||
CREATE TABLE IF NOT EXISTS teams (
|
CREATE TABLE IF NOT EXISTS teams (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
slug VARCHAR(255) NOT NULL UNIQUE,
|
slug VARCHAR(255) NOT NULL UNIQUE,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
created_by VARCHAR(255) NOT NULL,
|
created_by VARCHAR(255) NOT NULL,
|
||||||
settings JSONB DEFAULT '{}'::jsonb,
|
settings JSONB DEFAULT '{}'
|
||||||
CONSTRAINT check_team_slug_format CHECK (slug ~ '^[a-z0-9][a-z0-9-]*[a-z0-9]$' OR slug ~ '^[a-z0-9]$')
|
|
||||||
);
|
);
|
||||||
""",
|
""",
|
||||||
"""
|
"""
|
||||||
-- Create team_memberships table
|
|
||||||
CREATE TABLE IF NOT EXISTS team_memberships (
|
CREATE TABLE IF NOT EXISTS team_memberships (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
role VARCHAR(20) NOT NULL DEFAULT 'member',
|
role VARCHAR(50) NOT NULL DEFAULT 'member',
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
invited_by VARCHAR(255),
|
invited_by VARCHAR(255),
|
||||||
CONSTRAINT unique_team_membership UNIQUE (team_id, user_id),
|
CONSTRAINT team_memberships_unique UNIQUE (team_id, user_id),
|
||||||
CONSTRAINT check_team_role CHECK (role IN ('owner', 'admin', 'member'))
|
CONSTRAINT team_memberships_role_check CHECK (role IN ('owner', 'admin', 'member'))
|
||||||
);
|
);
|
||||||
""",
|
""",
|
||||||
# Add team_id column to projects table
|
|
||||||
"""
|
"""
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -322,10 +318,10 @@ def _run_migrations():
|
|||||||
WHERE table_name = 'projects' AND column_name = 'team_id'
|
WHERE table_name = 'projects' AND column_name = 'team_id'
|
||||||
) THEN
|
) THEN
|
||||||
ALTER TABLE projects ADD COLUMN team_id UUID REFERENCES teams(id) ON DELETE SET NULL;
|
ALTER TABLE projects ADD COLUMN team_id UUID REFERENCES teams(id) ON DELETE SET NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_projects_team_id ON projects(team_id);
|
||||||
END IF;
|
END IF;
|
||||||
END $$;
|
END $$;
|
||||||
""",
|
""",
|
||||||
# Create indexes for teams
|
|
||||||
"""
|
"""
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -335,21 +331,12 @@ def _run_migrations():
|
|||||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_teams_created_by') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_teams_created_by') THEN
|
||||||
CREATE INDEX idx_teams_created_by ON teams(created_by);
|
CREATE INDEX idx_teams_created_by ON teams(created_by);
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_teams_created_at') THEN
|
|
||||||
CREATE INDEX idx_teams_created_at ON teams(created_at);
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_team_memberships_team_id') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_team_memberships_team_id') THEN
|
||||||
CREATE INDEX idx_team_memberships_team_id ON team_memberships(team_id);
|
CREATE INDEX idx_team_memberships_team_id ON team_memberships(team_id);
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_team_memberships_user_id') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_team_memberships_user_id') THEN
|
||||||
CREATE INDEX idx_team_memberships_user_id ON team_memberships(user_id);
|
CREATE INDEX idx_team_memberships_user_id ON team_memberships(user_id);
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_team_memberships_role') THEN
|
|
||||||
CREATE INDEX idx_team_memberships_role ON team_memberships(role);
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_projects_team_id') THEN
|
|
||||||
CREATE INDEX idx_projects_team_id ON projects(team_id);
|
|
||||||
END IF;
|
|
||||||
END $$;
|
END $$;
|
||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
@@ -360,6 +347,7 @@ def _run_migrations():
|
|||||||
conn.execute(text(migration))
|
conn.execute(text(migration))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
logger.warning(f"Migration failed (may already be applied): {e}")
|
logger.warning(f"Migration failed (may already be applied): {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ class TestCreateDefaultAdmin:
|
|||||||
def test_create_default_admin_with_env_password(self):
|
def test_create_default_admin_with_env_password(self):
|
||||||
"""Test that ORCHARD_ADMIN_PASSWORD env var sets admin password."""
|
"""Test that ORCHARD_ADMIN_PASSWORD env var sets admin password."""
|
||||||
from app.auth import create_default_admin, verify_password
|
from app.auth import create_default_admin, verify_password
|
||||||
from app.models import User
|
|
||||||
|
|
||||||
# Create mock settings with custom password
|
# Create mock settings with custom password
|
||||||
mock_settings = MagicMock()
|
mock_settings = MagicMock()
|
||||||
@@ -20,23 +19,20 @@ class TestCreateDefaultAdmin:
|
|||||||
mock_db = MagicMock()
|
mock_db = MagicMock()
|
||||||
mock_db.query.return_value.count.return_value = 0 # No existing users
|
mock_db.query.return_value.count.return_value = 0 # No existing users
|
||||||
|
|
||||||
# Track all objects that get created
|
# Track the user that gets created
|
||||||
created_objects = []
|
created_user = None
|
||||||
|
|
||||||
def capture_object(obj):
|
def capture_user(user):
|
||||||
created_objects.append(obj)
|
nonlocal created_user
|
||||||
|
created_user = user
|
||||||
|
|
||||||
mock_db.add.side_effect = capture_object
|
mock_db.add.side_effect = capture_user
|
||||||
|
|
||||||
with patch("app.auth.get_settings", return_value=mock_settings):
|
with patch("app.auth.get_settings", return_value=mock_settings):
|
||||||
admin = create_default_admin(mock_db)
|
admin = create_default_admin(mock_db)
|
||||||
|
|
||||||
# Verify objects were created (user, team, membership)
|
# Verify the user was created
|
||||||
assert mock_db.add.called
|
assert mock_db.add.called
|
||||||
assert len(created_objects) >= 1
|
|
||||||
|
|
||||||
# Find the user object
|
|
||||||
created_user = next((obj for obj in created_objects if isinstance(obj, User)), None)
|
|
||||||
assert created_user is not None
|
assert created_user is not None
|
||||||
assert created_user.username == "admin"
|
assert created_user.username == "admin"
|
||||||
assert created_user.is_admin is True
|
assert created_user.is_admin is True
|
||||||
@@ -48,7 +44,6 @@ class TestCreateDefaultAdmin:
|
|||||||
def test_create_default_admin_with_default_password(self):
|
def test_create_default_admin_with_default_password(self):
|
||||||
"""Test that default password 'changeme123' is used when env var not set."""
|
"""Test that default password 'changeme123' is used when env var not set."""
|
||||||
from app.auth import create_default_admin, verify_password
|
from app.auth import create_default_admin, verify_password
|
||||||
from app.models import User
|
|
||||||
|
|
||||||
# Create mock settings with empty password (default)
|
# Create mock settings with empty password (default)
|
||||||
mock_settings = MagicMock()
|
mock_settings = MagicMock()
|
||||||
@@ -58,23 +53,20 @@ class TestCreateDefaultAdmin:
|
|||||||
mock_db = MagicMock()
|
mock_db = MagicMock()
|
||||||
mock_db.query.return_value.count.return_value = 0 # No existing users
|
mock_db.query.return_value.count.return_value = 0 # No existing users
|
||||||
|
|
||||||
# Track all objects that get created
|
# Track the user that gets created
|
||||||
created_objects = []
|
created_user = None
|
||||||
|
|
||||||
def capture_object(obj):
|
def capture_user(user):
|
||||||
created_objects.append(obj)
|
nonlocal created_user
|
||||||
|
created_user = user
|
||||||
|
|
||||||
mock_db.add.side_effect = capture_object
|
mock_db.add.side_effect = capture_user
|
||||||
|
|
||||||
with patch("app.auth.get_settings", return_value=mock_settings):
|
with patch("app.auth.get_settings", return_value=mock_settings):
|
||||||
admin = create_default_admin(mock_db)
|
admin = create_default_admin(mock_db)
|
||||||
|
|
||||||
# Verify objects were created
|
# Verify the user was created
|
||||||
assert mock_db.add.called
|
assert mock_db.add.called
|
||||||
assert len(created_objects) >= 1
|
|
||||||
|
|
||||||
# Find the user object
|
|
||||||
created_user = next((obj for obj in created_objects if isinstance(obj, User)), None)
|
|
||||||
assert created_user is not None
|
assert created_user is not None
|
||||||
assert created_user.username == "admin"
|
assert created_user.username == "admin"
|
||||||
assert created_user.is_admin is True
|
assert created_user.is_admin is True
|
||||||
|
|||||||
Reference in New Issue
Block a user