# Business rules — silicon_security

Evidence date: 2026-10-03.

Source: `git@bitbucket.org:overdrive/silicon_security.git`, `main` at `514489a`.

Rules are included only where code, a seeder, or a project document states them. **Inferred** means the code behaves this way and no requirement document states it as policy. Conflicting statements are left in conflict.

Credential values are omitted.

## Identity and access

### BR-01 — Login identifier is the username, and the username is an email address

**Implemented.** `Welcome::index` loads `users` by `username` from `inputEmail`. 2FA mail is sent to `$user['username']`. The seed username is `admin@overdrive.co.za`.

**Inferred** that usernames are always mailbox addresses. The column is `username`, not a separate email field, in the login query.

### BR-02 — Inactive users cannot sign in

**Implemented.** If `users.active` is 0, login stops with flash text "Your access has been revoked!. Please contact your administrator." Password is not checked.

### BR-03 — A correct password is not always enough

**Implemented.** After `password_verify`, `UserModel::check2faEmailConfirmationDate` decides whether the session is created or a code is emailed. Failed and successful attempts are written to login history (`status` 1 failure, `status` 2 success in `Welcome`).

The number of failures that create a `blocked_users_list` row was not found in `LoginHistory::updateLoginHistory` (it only inserts history). **Unknown / Requires Confirmation.**

### BR-04 — Document write access is denied when session role equals 3

**Implemented behaviour.** `Documents` upload, edit, update, and delete return a permission error when `session('role') == 3`. Comments in that file call 3 the Viewer role.

**Documented** in `docs/README.md` and `scope/Task_viewer_user_profile.md`: Viewer is read-only; System Admin has full CRUD; System User can upload, edit, and view; only System Admin and System User assign documents.

**Conflict.** `UserRole` seeder insert order is Developer, Admin, Company Admin, Company User, System Admin, System User, Viewer. Role id 3 in an empty table would be Company Admin, not Viewer. `Users::searchDocumentsForSharing` treats every role other than 3 as allowed to list all documents.

**Requires Confirmation** before anyone maps production role ids to these names.

### BR-05 — Page access is a menu grant, not a role name

**Inferred** from `Authorization` and `useraccess_helper.php`. The first URL segment must exist as `user_menu.url`. The session role must have a `user_access` row for that menu. Missing menu redirects to `/`. Missing grant redirects to `blocked`.

Exceptions that skip this check are listed in `app/Config/Filters.php` (`home`, `blocked`, chatGPT, rag, auth URLs, `cronJobs`, `slack`, `organisations`, `store-api`, and others).

### BR-06 — A restricted user may open documents only inside a weekly clock window

**Implemented.** `users.access_time_restricted` = 1 and a `user_access_times` row: `days_of_week` JSON must include today's `date('N')` (1 Monday through 7 Sunday), and the current `H:i:s` must be between `start_time` and `end_time` inclusive. Otherwise `documents*` redirects to `blocked`.

No access-time row means allow, even when the flag is set (`isWithinAccessTime` returns true when `getUserAccessTimes` is empty).

**Documented** as a Viewer restriction. **Implemented** for any user with the flag. The filter does not compare the role. **Conflict.**

### BR-07 — Viewer document visibility is owner or a live assignment

**Implemented** in `DocumentModel` when role is 3.

A document is visible if `documents.created_by` is the session user, or `user_documents` has `active` = 1 and `checkDocumentAccess` allows it now. Other roles skip this filter and see every `documents.active` = 1 row that matches the other filters.

Soft delete is not used. Removal from lists is `active` = 0.

## Document sharing

### BR-08 — Three access types

**Documented** in `scope/Task_viewer_user_profile.md`. **Implemented** in `UserDocumentModel::checkDocumentAccess`.

| `access_type` | Allow when |
|---|---|
| `time_limit` | Now is on or after `access_start_time` (if set) and on or before `access_end_time` (if set) |
| `login_active_time` | `last_login_time` is set and now is within `login_active_hours` hours after it |
| `permanent` | The assignment row is active |
| other | Never |

Empty `last_login_time` denies `login_active_time` even if hours are set.

### BR-09 — One active assignment per user and document

**Implemented.** `assignDocumentToUser` updates the existing `active` = 1 row for that pair instead of inserting another. Revoke sets `active` = 0 and does not delete the row.

### BR-10 — Profile URLs do not carry the target user id

**Documented** in the scope and changelog. **Implemented** by `users/setSelectedUser` then `users/profile`, which reads `selectedUserID` from the session (`Users::searchDocumentsForSharing`).

### BR-11 — Sharing does not email the viewer

**Documented** as out of scope in `scope/Task_viewer_user_profile.md`. No assignment-mail path was found. **Inferred** that silence is the current behaviour, not an accident discovered only in code.

Expiry warnings and an access-analytics dashboard are the same kind of documented exclusion.

### BR-12 — Upload types and title

**Implemented.**

- Extensions: `pdf`, `jpg`, `jpeg`, `png`. Message: "Invalid file type. Allowed: PDF, JPG, PNG".
- Title required, length 3 to 255. `document_type_id` required integer. Temp path required on the save step.
- Role 3 cannot upload.
- Size limit is PHP `upload_max_filesize` and `post_max_size`, not an application constant.

### BR-13 — Seeded document types

**Implemented** in `DocumentTypes` seeder, inserted only when `type_code` is absent:

- Legislation / `legislation` — "Legislative documents and regulations"
- Labels / `labels` — "Label documents"
- Minutes / `minutes` — "Meeting minutes and notes"

The UI tabs follow whatever types exist. The changelog says the screen tabs are Search, Legislation, Labels, Minutes. **Documented** and consistent with the seeder if those three rows exist.

## Chat

### BR-14 — A user cannot continue someone else's conversation id

**Documented** as a fix in `docs/CHAT_SECURITY_AUDIT.md`. **Implemented** in `Chat::send` and `Chat::stream`: if `getConversation($conversationId, $userID)` is empty, the id is dropped and a new conversation is created. A warning is logged.

History and conversation list require a session user id and return 401 when it is missing.

### BR-15 — Memory is extracted only after a short conversation

**Documented** in `docs/IMPLEMENTATION_SUMMARY.md`. Minimum length two messages. Duplicate memories are skipped. Semantic recall uses similarity at least 0.3 and at most five past conversations. At most five documents get context updates per conversation. Queries shorter than three characters skip semantic search.

These figures were not re-read line by line in the helper. Provenance: **Documented**. Treat a mismatch with `chat_memory_helper.php` as **Requires Confirmation** if behaviour is tested later.

## Email and cron

### BR-16 — Outbound product mail is queued

**Documented** and **Implemented.** `mail_helper.php` `sendMail` can queue. `sent_queue.processed` 0 means not sent, 1 means sent. `send_date` null means immediate; a timestamp means not before that time.

### BR-17 — Cron is an open HTTP GET

**Implemented.** `cronJobs` is excluded from `isLoggedIn` and `isGranted`. The runner calls functions named on `cronjob_actions` rows (`sendScheduledEmails`, `getAllUnsentEmails`, `checkServerLogs` exist on the controller).

**Inferred** that anyone who can reach the host can trigger the runner. No shared secret was seen on the route. **Requires Confirmation** if a network control outside this repo protects it.

### BR-18 — Each cron pass sends a bounded batch

**Documented** as `items_process_total`, default 10, for both scheduled and immediate mail. **Requires Confirmation** against `CronJobs.php` constants if operations depend on the exact cap.

## Data rules

### BR-19 — Application database is MySQL

**Implemented** in `app/Config/Database.php`: default `DBDriver` `MySQLi`, host `localhost`, database name empty in config (overridden by environment). Tests use SQLite `:memory:`.

`MySQLVectorStore` comments require MySQL 9.1+ `VECTOR` when `VECTOR_STORE_TYPE=mysql`. That is a code comment, not a checked server version.

### BR-20 — Documents are not served as public files

**Inferred.** Upload path is `WRITEPATH . 'uploads/documents/'`. Download sets `Content-Type` from the stored mime and sends the file from the controller. A direct public URL for the file was not found.

### BR-21 — Organisations API is described but not present

**Documented** only as comments in `Routes.php`: `POST /organisations/login` returns a bearer token; later calls require it. `$routes->resource('organisations')` is registered. No organisations controller is in `app/Controllers`. **Not an enforceable rule** until that controller exists.

`JwtLibrary::generate_jwt` uses the secret argument or the literal `secret`. **Implemented** default. **Requires Confirmation** that no deployed route relies on it.

## Seeded access

### BR-22 — A local developer user and a partial menu grant are seeded

**Implemented** in `Users`, `UserAccess`, `UserMenu`, `UserRole`.

- One user, role 1, active.
- `UserAccess` grants role 1 menu categories 1, 2, 3 and menu ids 1, 2, 3, 4. Menu ids in the menu seeder are assigned by the database. The fourth menu inserted is CRUD Generator; Media Files is fifth; System Settings is sixth. Role 1 is therefore not granted every seeded menu if ids are 1-based in insert order. **Inferred** from insert order. **Requires Confirmation** on a real database where ids may already exist.

`UserRole::run` is invoked from the Users seeder after the user insert. Role names are created only when missing. Existing databases keep their ids.

## Rules that are not established

- Which company a Company Admin or Company User may see. `setCompanyUsersAndAdminSessionVars` exists. The rule is **Unknown**.
- Whether transport methods and fault types in system settings still apply, given the missing fault-finder and company controllers.
- Production retention of audit logs, chat logs, and login history. No purge rule was found.
- Password length and reset-link lifetime. Not read in full. **Unknown**.
- A legal or contractual difference between Legislation, Labels, and Minutes beyond the seeder descriptions. **Unknown**.
