Identity Management

Plant Tracer uses several identities with different ownership and security properties. Do not overload the word admin for all of them.

Server Sender Identity

SERVER_EMAIL is the application sender address used for outgoing Plant Tracer email. In the Lambda-only stack this is admin@planttracer.com. This address is not a course administrator and is not a login account for a teacher. It is the verified SES identity that the stack is allowed to send from.

SERVER_EMAIL_NAME is the display name paired with SERVER_EMAIL in the MIME From header. The default stack value is Plant Tracer.

The Lambda-only stack defines both values and passes them to lambda-web as environment variables. The mailer uses:

  • SERVER_EMAIL as the bare SMTP/SES envelope sender and SES Source.

  • SERVER_EMAIL_NAME <SERVER_EMAIL> as the human-readable MIME From header.

Keeping those values separate matters because SES IAM permissions and ses:FromAddress conditions are scoped to the sender email address, while users should see a readable product name in their mail client.

User Identity

Plant Tracer users are identified by email address. The users table stores the durable user profile:

  • user_id: the stable internal identifier. It is generated by the application and should be used for joins, ownership checks, and permissions.

  • email: the user’s login and contact address. It is also stored in the unique_emails table so that registration can enforce one user profile per email address.

  • user_name: the user’s display name. This is the name shown in page headers, movie lists, course user lists, and some movie metadata defaults.

  • enabled: whether the user can authenticate.

  • primary_course_id and primary_course_name: the course context used by normal page rendering and uploads.

  • courses: the course ids in which the user is enrolled.

  • admin_for_courses: the course ids for which the user has course administrator privileges.

The email address is the human-facing identity, but user_id is the durable authorization and ownership key. Do not store relationships by user name, and do not assume that a display name is unique.

Names

user_name is editable display metadata, not an authentication credential. The browser presents this as “Name” on pages such as /list, /upload, and /audit. Those pages use a pencil editor to send updates through POST /api/set-metadata for the active user.

Changing user_name changes future display of the user profile, but it does not change existing denormalized movie owner names that may already be stored on movie records. Code that needs the current user name should read the user profile; code that needs historical movie display should tolerate the movie row having its own stored user_name value.

The same pages currently expose the user’s email address for editing. Email is security-sensitive because it is the login identity and uniqueness key. Backend email changes must update both users.email and unique_emails.email transactionally; otherwise two user profiles could claim the same address or a login link could be sent to an address that no longer maps cleanly to the user.

Course Administrators

ADMIN_EMAIL and ADMIN_NAME refer to a course administrator, typically the instructor or teacher for a course. They are data-plane identities stored in DynamoDB user/course records. They are not stack sender identities.

A course administrator can:

  • manage the course they administer;

  • register students for that course;

  • receive course setup/login email through the configured Plant Tracer sender.

Course administrators should be created or updated through dbutil and operator Makefile targets. They should not be configured as SAM stack parameters because courses and users live in DynamoDB, and DynamoDB data now outlives individual lambda-only stacks.

Operators can list current course administrators with make admin-list. The target runs src/dbutil.py admin-list against the selected AWS/DynamoDB environment and prints each administrator’s display name, email address, user_id, and administered courses. Set AWS_REGION and DYNAMODB_TABLE_PREFIX for the target environment before running it; local development defaults still use DynamoDB Local and the demo- prefix. The operator-facing administrator list is built by app.course_management.list_admins so future web administration pages can reuse the same read model.

Operators can create or update course administrators with make admin-create. The target runs src/dbutil.py admin-create. In an interactive terminal it asks for administrator email/name, lists courses where that user is not already an administrator, and accepts one or more selected courses. Non-interactive automation can pass ADMIN_CREATE_FLAGS, for example ADMIN_CREATE_FLAGS="--admin_email teacher@example.edu --admin_name 'Teacher Name' --course_id BIO101 --planttracer_endpoint https://example.planttracer.com". By default the command sends the administrator a course setup/login email through the configured mail path. Use --planttracer_endpoint or set HOSTNAME/DOMAIN so generated links point at the intended stack. Use --no-send-email for data-only dry runs, or MAILER_DRY_RUN=true to render mail without sending it.

Operators can create courses with make course-create. The target runs src/dbutil.py create-course --send-email. In an interactive terminal it asks for course id/number, course name, and course administrator. Existing course administrators are listed first so the operator can select one; pressing Enter creates a new administrator from the prompted email/name. Non-interactive automation can pass COURSE_CREATE_FLAGS, for example COURSE_CREATE_FLAGS="--course_id BIO101 --course_name 'Plant Biology 101' --admin_email teacher@example.edu --admin_name 'Teacher Name' --planttracer_endpoint https://example.planttracer.com". The reusable course/admin creation logic is in app.course_management so that future web administration pages can call the same operation without shelling out to dbutil. If the course already exists with the same name, the command treats that as an operator retry and ensures the selected administrator relationship and email step. If the existing course name differs, the command fails rather than silently reusing the wrong course id.

For a deployed Lambda-only stack, prefer make sam-course-create. It reads stack_name from the selected ignored SAM_CONFIG file, resolves the stack’s DynamoDBTablePrefix, ApplicationUrl, and MailerDryRun settings from CloudFormation, and then delegates to src/dbutil.py create-course --send-email. Pass the same COURSE_CREATE_FLAGS used by make course-create. This keeps course initialization separate from stack deployment while reducing the chance of creating course data in the wrong table prefix or sending links for the wrong host.

The demo course has its own narrower target: make demo-course-create. That target runs src/dbutil.py create-demo-course and ensures only the durable demo course data exists: demo-course, the demo course administrator, the demo user, and the fixed demo-mode API key. It does not create tables, upload objects, or seed demo movies. Run it after deploying a demo stack, or any time the selected DynamoDB prefix needs the demo course repaired. The command uses the current AWS/DynamoDB environment, so set AWS_REGION and DYNAMODB_TABLE_PREFIX for the target database before running it. For local development with local services and seeded demo movies, continue to use make make-local-demo. That target runs table creation, demo course creation, and sample movie seeding as separate dbutil commands; there is no combined dbutil create-demo command in the lambda-only workflow.

Courses

Courses are durable application data. COURSE_ID and COURSE_NAME should not be stack parameters or Lambda runtime environment variables. Course creation should be an explicit, idempotent post-deploy data initialization step against the selected DYNAMODB_TABLE_PREFIX.

Course Keys

course_key is a course registration secret, not a user password and not an API key. It is stored on the courses table and indexed so that the registration page can find the course from the key.

The public registration flow is:

  1. A user enters email address, display name, and course key on /register.

  2. POST /api/register validates the email and course key.

  3. register_email creates or updates the user, enrolls the user in the course, and records the course_users join row.

  4. The application creates a new api_key for that user.

  5. The mailer sends a magic login link to the user’s email address.

Course keys only authorize enrollment in a course. After enrollment, normal page and API authorization is based on api_key and course membership.

Authorization Model

The authenticated user comes from validate_api_key(api_key). A valid key returns the linked user only if both the key and the user are enabled.

Normal ownership and permission checks use:

  • user_id for movie ownership and profile updates.

  • courses and course_users for course membership.

  • admin_for_courses and admins_for_course for course administrator privileges.

  • primary_course_id for the current course context on rendered pages and uploads.

Course administrators are still normal users. Their extra authority applies to the courses listed in their administrator fields; it does not make their email address a stack sender identity and does not make them a global application administrator.

Dry-Run Mail

Non-production stacks whose operators cannot send SES mail as SERVER_EMAIL can set MailerDryRun=true. Dry-run mode renders mail and writes it to logs instead of sending it. Use this only with test users and test data because the rendered email includes login links and API keys.