lablup / lablup/backend.ai

Add an abstract authentication plugin base class

Open
#14,001 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
670
Forks
183
Avg merge
17h 7m
Merged PRs (30d)
358

Description

Add the abstract authentication plugin class and its entry-point group, so an authentication integration declares one contract instead of reaching into manager internals. This issue delivers the abstraction and its loading; porting the existing integrations is separate.

```python
# entry-point group: backendai_auth_v1 — at most one plugin may be loaded

@dataclass
class HTTPRequestData:
headers: CIMultiDictProxy[str] # multi-dict form kept: a plain dict would drop
body: Mapping[str, Any] # repeated keys and case-insensitive header lookup
cookies: Mapping[str, str]
query_params: MultiMapping[str]

# LookupKey subclasses, one per shape. kind() feeds the metric, to_dict() the audit record.
type UserLookupKey = (
UserIdLookupKey | UserEmailLookupKey | UserNameLookupKey | UserAccessKeyLookupKey
)

class AbstractAuthPlugin(AbstractPlugin):
@classmethod
@abstractmethod
def lookup_retry_count(cls) -> int: ... # zero or less: no retry, one lookup

async def generate_lookup_data(
self, request: HTTPRequestData
) -> UserLookupKey | None: ...

async def on_user_lookup_success(self, user: UserData) -> None: ...

async def on_user_lookup_error(self, error: BackendAIError) -> None: ...
```

### Contract

- **Verification happens in the request-to-key step** — signature checks, token decryption, directory binds and external verifier calls. The manager resolves the key it is given and never verifies the credential itself, so a plugin that derives a key without validating it grants access to anyone who can shape the request. The method name reads like a lookup, so the contract has to carry this.
- **Declining** — returning nothing means the request carries no credential this plugin handles. The manager falls back to password authentication. Every existing integration relies on this.
- **Provisioning and retry** — every failed attempt goes to the plugin's callback, which decides what happens next: returning lets the manager try the lookup again, raising aborts at once. The manager does not classify the failure. It clamps the declared retry count with `max(count, MIN_LOOKUP_RETRY_COUNT)` so the lookup always runs at least once, rather than rejecting the plugin at class definition.
- **Concurrent provisioning** — a conflict raised while two simultaneous sign-ins provision the same account is a normal path that the retry resolves. The plugin must not let it escape.
- **No account enumeration** — an exhausted lookup fails with the same generic error the password path uses, never the not-found error, so a failed sign-in cannot probe which accounts exist.
- **Success callback** — side effects only. It receives the account the flow already resolved, returns nothing and cannot emit a response.
- **No ORM or row types cross the boundary** — the callback receives a dataclass. The authorize flow carries the same dataclass end to end instead of a SQLAlchemy row mapping.
- **Single plugin** — the conflict is detected at plugin discovery, before `init()`, and the manager refuses to start with an error naming both plugins and the setting that disables one.
- **Configuration** — one namespace per plugin, shared by all of its components.

### Why a LookupKey rather than a flat record

The manager already had the concept. `LookupKey` in `actions/v2/lookup/base.py` is documented as "the external key a lookup resolves" and has 34 subclasses; the model layer has the matching `DataLookup`, "reads one entity by a key that is not its primary key".

- A key has one shape, so there is no declared-order rule to specify and no way to hand over a record that names no account.
- `kind()` keys the authentication metric by login path, and `to_dict()` gives the audit record, both for free.
- The manager side resolves the key by matching on its type, which the type checker proves exhaustive.

JIRA Issue: BA-7504

Contributor guide

Open the contributing guide

Research direction

Start with LookupKey and its 34 subclasses in actions/v2/lookup/base.py, then trace the authentication plugin entry-point group backendai_auth_v1 through discovery and loading before init(). Done means the abstract plugin contract, request-to-key verification boundary, callbacks, retry behavior, dataclass handoff, and single-plugin conflict handling are represented without porting existing integrations.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
authentication, backend-api-design
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.