apache/cassandra · error · UnauthorizedException

Only superusers can remove identity bindings from a role…

Error message

Only superusers can remove identity bindings from a role with superuser status

What it means

DropIdentityStatement.authorize prevents non-superusers from detaching an identity (e.g., an mTLS identity/authorization identity) that is bound to a role carrying superuser status, to avoid regular admins weakening superuser authentication. It throws UnauthorizedException when Roles.hasSuperuserStatus on the target role is true and the caller is not a superuser.

Solutions

  1. Perform the DROP IDENTITY as a superuser.
  2. First (as superuser) demote the target role: ALTER ROLE <role> WITH SUPERUSER = false, then remove the identity with the regular admin.
  3. Re-scope the automation to use a superuser credential only for identity-management operations.

Example fix

// before (non-superuser)
DROP IDENTITY 'CN=svc,OU=sec';
// after (as superuser)
DROP IDENTITY 'CN=svc,OU=sec';
// or, as superuser first:
ALTER ROLE svc_role WITH SUPERUSER = false; -- then non-superuser admin may drop the identity
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check (as caller): only attempt identity drops with a superuser connection
if (!currentUserIsSuperuser(session) && identityBoundToSuperuserRole(identity)) throw new IllegalStateException('requires superuser');

Try / catch

try { session.execute(dropIdentityCql); } catch (UnauthorizedException e) { if (e.getMessage().contains("Only superusers can remove identity bindings")) { /* escalate to superuser connection or demote target role first */ } else throw e; }

Prevention

When it happens

Trigger: A non-superuser executes DROP IDENTITY <identity> where the identity is bound (roleForIdentity) to a role whose superuser status is true, as resolved before the check.

Common situations: Security teams rotating certificates/identities with an admin account that is not a superuser; automation with scoped credentials attempting identity cleanup; confusion between role-admin permissions and superuser-only identity operations.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of apache/cassandra@88fd0f6a0e (2026-09-10). Data as JSON: /api/errors/7124ab4088444dd7. Report an issue: GitHub.

Appendix: source

Thrown at src/java/org/apache/cassandra/cql3/statements/DropIdentityStatement.java:70

        String roleForIdentity = DatabaseDescriptor.getRoleManager().roleForIdentity(identity);

        if (roleForIdentity == null)
        {
            checkPermission(state, Permission.DROP, RoleResource.root());
        }
        else
        {
            // Check permission for the target role, i.e. we were granted permission to DROP ROLE
            // for the target identity, this should allow us to drop the identity to role mapping
            checkPermission(state, Permission.DROP, RoleResource.role(roleForIdentity));

            if (!state.getUser().isSuper())
            {
                // If the current user is a regular user and the target role is an admin role
                // we disallow the operation. Only a superuser can remove an identity bound to
                // a role with superuser status
                if (Roles.hasSuperuserStatus(RoleResource.role(roleForIdentity)))
                    throw new UnauthorizedException("Only superusers can remove identity bindings from a role with superuser status");
            }
        }
    }

    @Override
    public void validate(ClientState state)
    {
        state.ensureNotAnonymous();

        if (!ifExists && !DatabaseDescriptor.getRoleManager().isExistingIdentity(identity))
        {
            throw new InvalidRequestException(String.format("identity '%s' doesn't exist", identity));
        }
    }

    @Override
    public AuditLogContext getAuditLogContext()
    {

View on GitHub (pinned to 88fd0f6a0e)