BetterIAM
Governance

Terms of use

Versioned agreements such as acceptable-use policies that members accept, with enforcement through ordinary policy statements.

@better-iam/server@better-iam/react@better-iam/vuegovernance.mdpolicies.mdagreements.tsindex.tsx

Many organizations must show that people agreed to the rules before they got access: an acceptable-use policy, an NDA, data-handling rules for customer data. Emailing a PDF proves nothing, and tracking acceptance in a separate tool means access and agreement drift apart.

put the terms next to the access. An agreement is a versioned text a tenant asks its members to accept. Better IAM records who accepted which version and exposes that to every authorization decision, so enforcing it is an ordinary statement, such as "deny documents until the terms are accepted", that combines with everything else.

Publish an agreement

agreements.create (iam:agreements:manage) publishes an agreement at version 1. Members are asked to accept it from then on.

await iam.api.agreements.create(credential, {
  tenantId,
  name: 'Acceptable use',
  content: 'Use company systems for work. Report incidents within 24 hours.',
  reacceptAfterDays: 365,
});

Prop

Type

A tenant holds at most 50 agreements.

Manage agreements

  • agreements.update (iam:agreements:manage) edits an agreement. With newVersion: true, the change becomes the next version and everyone must accept again; use it when the rules changed. Without it, for a typo fix, existing acceptances stay valid. reacceptAfterDays: null removes the lapse.
  • agreements.list (iam:agreements:read) lists the tenant's agreements with their current version.
  • agreements.status({ tenantId, agreementId }) (iam:agreements:read) reports who accepted the current version and which active people still owe it (an outdated version, a lapsed acceptance, or none). Use it for compliance reports and to chase people who have not accepted.
  • agreements.delete (iam:agreements:manage) removes an agreement together with its acceptances.

Accept

Members accept for themselves, and need no permission for it:

const mine = await iam.api.agreements.listMine({ token }, { tenantId });
for (const agreement of mine.filter((item) => item.required && !item.accepted))
  await iam.api.agreements.accept(
    { token },
    { tenantId, agreementId: agreement.id, version: agreement.version },
  );
  • agreements.listMine({ tenantId }) returns every agreement of the tenant with its text and whether the caller has accepted its current version. Use it to show the terms.
  • agreements.accept({ tenantId, agreementId, version }) records acceptance of the version the person was shown. Passing the version means nobody accepts text they never saw: once the agreement changed, the call fails with VERSION_CONFLICT. It is audited as agreement:accept.
  • Both need an ordinary session of the tenant. An administrator using cannot accept for someone (IMPERSONATION_RESTRICTED), and accept nothing (nothing is pending for them).

The console shows members the required agreements they owe at the top of every page with an "I accept" button, and its Terms of use page publishes, edits, and tracks them.

In React and Vue

To show the same banner in your own application, use the useAgreements hook. It returns the person's agreements, the pending ones (required and not accepted in their current version), and an accept function that records acceptance and reloads.

components/terms-banner.tsx
import { useAgreements } from 'better-iam/react';

export function TermsBanner({ tenantId }: { tenantId: string }) {
  const { pending, accept } = useAgreements({ tenantId });
  if (!pending.length) return null;
  return (
    <div role="alert">
      {pending.map((agreement) => (
        <p key={agreement.id}>
          {agreement.name} <button onClick={() => accept(agreement)}>I accept</button>
        </p>
      ))}
    </div>
  );
}

The Vue composable of the same name, from better-iam/vue, accepts its input as a ref or getter and returns the same fields with the data as refs. Both take enabled: false to skip loading. See React and Vue.

Enforce with a policy

Recording acceptance is only half the job; access should wait for it. Instead of a special switch, Better IAM gives policies two facts about each person, so you decide exactly which access depends on which terms. Every evaluation for a person in their own tenant carries these context keys:

  • principal.agreements: the names of the agreements accepted in their current version.
  • principal.pendingAgreements: how many required ones are still owed.

A deny holds back access until every required agreement is accepted:

Hold back documents until the terms are accepted
{
  "effect": "deny",
  "actions": ["documents:*"],
  "resources": ["*"],
  "conditions": { "NumericGreaterThan": { "principal.pendingAgreements": 0 } }
}

A condition on an allow statement can grant something only to people who accepted an optional agreement, such as a beta program:

{ "ArrayContains": { "principal.agreements": ["Beta program"] } }

When a person is denied because of pending terms, access paths report an accept-agreements path listing what to accept.

Agreements as code

Agreements are part of configuration as code: name, content, url, required (default true), and reacceptAfterDays, matched by name regardless of case. A changed content publishes a new version, so everyone accepts the new text; other edits keep acceptances. Changing them needs iam:agreements:manage.

Publishing agreements is guarded by enforced (change safety). Members accepting agreements is not, so schedule the invariant monitor if an invariant depends on them.

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page